// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2011 Red Hat, Inc.
 * Copyright (C) 2013 Thomas Bechtold <thomasbechtold@jpberlin.de>
 */

#include "nm-default.h"

#include "nm-config.h"

#include <stdio.h>

#include "nm-utils.h"
#include "devices/nm-device.h"
#include "NetworkManagerUtils.h"
#include "nm-core-internal.h"
#include "nm-keyfile/nm-keyfile-internal.h"
#include "nm-keyfile/nm-keyfile-utils.h"

#define DEFAULT_CONFIG_MAIN_FILE        NMCONFDIR "/NetworkManager.conf"
#define DEFAULT_CONFIG_DIR              NMCONFDIR "/conf.d"
#define DEFAULT_CONFIG_MAIN_FILE_OLD    NMCONFDIR "/nm-system-settings.conf"
#define DEFAULT_SYSTEM_CONFIG_DIR       NMLIBDIR  "/conf.d"
#define RUN_CONFIG_DIR                  NMRUNDIR  "/conf.d"
#define DEFAULT_NO_AUTO_DEFAULT_FILE    NMSTATEDIR "/no-auto-default.state"
#define DEFAULT_INTERN_CONFIG_FILE      NMSTATEDIR "/NetworkManager-intern.conf"
#define DEFAULT_STATE_FILE              NMSTATEDIR "/NetworkManager.state"

/*****************************************************************************/

struct NMConfigCmdLineOptions {
	char *config_main_file;
	char *intern_config_file;
	char *config_dir;
	char *system_config_dir;
	char *state_file;
	char *no_auto_default_file;
	char *plugins;
	NMConfigConfigureAndQuitType configure_and_quit;

	gboolean is_debug;
	char *connectivity_uri;

	/* We store interval as signed internally to track whether it's
	 * set or not via GOptionEntry
	 */
	int connectivity_interval;
	char *connectivity_response;

	/* @first_start is not provided by command line. It is a convenient hack
	 * to pass in an argument to NMConfig. This makes NMConfigCmdLineOptions a
	 * misnomer.
	 *
	 * It is true, if NM is started the first time -- contrary to a restart
	 * during the same boot up. That is determined by the content of the
	 * /run/NetworManager state directory. */
	bool first_start;
};

typedef struct {
	NMConfigState p;
} State;

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE_BASE (
	PROP_CMD_LINE_OPTIONS,
	PROP_ATOMIC_SECTION_PREFIXES,
);

enum {
	SIGNAL_CONFIG_CHANGED,
	LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = { 0 };

typedef struct {
	NMConfigCmdLineOptions cli;

	NMConfigData *config_data;
	NMConfigData *config_data_orig;

	char *config_dir;
	char *system_config_dir;
	char *no_auto_default_file;
	char *intern_config_file;

	char *log_level;
	char *log_domains;

	NMConfigConfigureAndQuitType configure_and_quit;

	char **atomic_section_prefixes;

	/* The state. This is actually a mutable data member and it makes sense:
	 * The regular config is immutable (NMConfigData) and can old be swapped
	 * as a whole (via nm_config_set_values() or during reload). Thus, it can
	 * be changed, but it is still immutable and is swapped atomically as a
	 * whole. Also, we emit a config-changed signal on that occasion.
	 *
	 * For state, there are no events. You can query it and set it.
	 * It only gets read *once* at startup, and later is cached and only
	 * written out to disk. Hence, no need for the immutable dance here
	 * because the state changes only on explicit actions from the daemon
	 * itself. */
	State *state;

	/* the hash table of device states. It is only loaded from disk
	 * once and kept immutable afterwards.
	 *
	 * We also read all state file at once. We don't want to support
	 * that they are changed outside of NM (at least not while NM is running).
	 * Hence, we read them once, that's it. */
	GHashTable *device_states;

	char **warnings;
} NMConfigPrivate;

struct _NMConfig {
	GObject parent;
	NMConfigPrivate _priv;
};

struct _NMConfigClass {
	GObjectClass parent;
};

static void nm_config_initable_iface_init (GInitableIface *iface);

G_DEFINE_TYPE_WITH_CODE (NMConfig, nm_config, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, nm_config_initable_iface_init);
                         )

#define NM_CONFIG_GET_PRIVATE(self) _NM_GET_PRIVATE (self, NMConfig, NM_IS_CONFIG)

/*****************************************************************************/

#define _NMLOG_DOMAIN      LOGD_CORE
#define _NMLOG(level, ...) __NMLOG_DEFAULT (level, _NMLOG_DOMAIN, "config", __VA_ARGS__)

/*****************************************************************************/

static void _set_config_data (NMConfig *self, NMConfigData *new_data, NMConfigChangeFlags reload_flags);

/*****************************************************************************/

int
nm_config_parse_boolean (const char *str,
                         int default_value)
{
	return _nm_utils_ascii_str_to_bool (str, default_value);
}

int
nm_config_keyfile_get_boolean (const GKeyFile *keyfile,
                               const char *section,
                               const char *key,
                               int default_value)
{
	gs_free char *str = NULL;

	g_return_val_if_fail (keyfile != NULL, default_value);
	g_return_val_if_fail (section != NULL, default_value);
	g_return_val_if_fail (key != NULL, default_value);

	str = g_key_file_get_value ((GKeyFile *) keyfile, section, key, NULL);
	return nm_config_parse_boolean (str, default_value);
}

gint64
nm_config_keyfile_get_int64 (const GKeyFile *keyfile,
                             const char *section,
                             const char *key,
                             guint base,
                             gint64 min,
                             gint64 max,
                             gint64 fallback)
{
	gint64 v;
	int errsv;
	char *str;

	g_return_val_if_fail (keyfile, fallback);
	g_return_val_if_fail (section, fallback);
	g_return_val_if_fail (key, fallback);

	str = g_key_file_get_value ((GKeyFile *) keyfile, section, key, NULL);
	v = _nm_utils_ascii_str_to_int64 (str, base, min, max, fallback);
	if (str) {
		errsv = errno;
		g_free (str);
		errno = errsv;
	}
	return v;
}

char *
nm_config_keyfile_get_value (const GKeyFile *keyfile,
                             const char *section,
                             const char *key,
                             NMConfigGetValueFlags flags)
{
	char *value;

	if (NM_FLAGS_HAS (flags, NM_CONFIG_GET_VALUE_RAW))
		value = g_key_file_get_value ((GKeyFile *) keyfile, section, key, NULL);
	else
		value = g_key_file_get_string ((GKeyFile *) keyfile, section, key, NULL);

	if (!value)
		return NULL;

	if (NM_FLAGS_HAS (flags, NM_CONFIG_GET_VALUE_STRIP))
		g_strstrip (value);

	if (   NM_FLAGS_HAS (flags, NM_CONFIG_GET_VALUE_NO_EMPTY)
	    && !*value) {
		g_free (value);
		return NULL;
	}

	return value;
}

void
nm_config_keyfile_set_string_list (GKeyFile *keyfile,
                                   const char *group,
                                   const char *key,
                                   const char *const* strv,
                                   gssize len)
{
	gsize l;
	char *new_value;

	if (len < 0)
		len = strv ? g_strv_length ((char **) strv) : 0;

	g_key_file_set_string_list (keyfile, group, key, strv, len);

	/* g_key_file_set_string_list() appends a trailing separator to the value.
	 * We don't like that, get rid of it. */

	new_value = g_key_file_get_value (keyfile, group, key, NULL);
	if (!new_value)
		return;

	l = strlen (new_value);
	if (l > 0 && new_value[l - 1] == NM_CONFIG_KEYFILE_LIST_SEPARATOR) {
		/* Maybe we should check that value doesn't end with "\\,", i.e.
		 * with an escaped separator. But the way g_key_file_set_string_list()
		 * is implemented (currently), it always adds a trailing separator. */
		new_value[l - 1] = '\0';
		g_key_file_set_value (keyfile, group, key, new_value);
	}
	g_free (new_value);
}

/*****************************************************************************/

const char *const*
nm_config_get_warnings (NMConfig *config)
{
	return (const char *const *) NM_CONFIG_GET_PRIVATE (config)->warnings;
}

void
nm_config_clear_warnings (NMConfig *config)
{
	nm_clear_pointer (&NM_CONFIG_GET_PRIVATE (config)->warnings, g_strfreev);
}

NMConfigData *
nm_config_get_data (NMConfig *config)
{
	g_return_val_if_fail (config != NULL, NULL);

	return NM_CONFIG_GET_PRIVATE (config)->config_data;
}

/* The NMConfigData instance is reloadable and will be swapped on reload.
 * nm_config_get_data_orig() returns the original configuration, when the NMConfig
 * instance was created. */
NMConfigData *
nm_config_get_data_orig (NMConfig *config)
{
	g_return_val_if_fail (config != NULL, NULL);

	return NM_CONFIG_GET_PRIVATE (config)->config_data_orig;
}

const char *
nm_config_get_log_level (NMConfig *config)
{
	g_return_val_if_fail (config != NULL, NULL);

	return NM_CONFIG_GET_PRIVATE (config)->log_level;
}

const char *
nm_config_get_log_domains (NMConfig *config)
{
	g_return_val_if_fail (config != NULL, NULL);

	return NM_CONFIG_GET_PRIVATE (config)->log_domains;
}

NMConfigConfigureAndQuitType
nm_config_get_configure_and_quit (NMConfig *config)
{
	return NM_CONFIG_GET_PRIVATE (config)->configure_and_quit;
}

gboolean
nm_config_get_is_debug (NMConfig *config)
{
	return NM_CONFIG_GET_PRIVATE (config)->cli.is_debug;
}

gboolean
nm_config_get_first_start (NMConfig *config)
{
	return NM_CONFIG_GET_PRIVATE (config)->cli.first_start;
}

const char *
nm_config_get_no_auto_default_file (NMConfig *config)
{
	return NM_CONFIG_GET_PRIVATE (config)->no_auto_default_file;
}

/*****************************************************************************/

static char **
no_auto_default_from_file (const char *no_auto_default_file)
{
	gs_free char *data = NULL;
	const char **list = NULL;
	gsize i;

	if (   no_auto_default_file
	    && g_file_get_contents (no_auto_default_file, &data, NULL, NULL))
		list = nm_utils_strsplit_set (data, "\n");

	if (list) {
		for (i = 0; list[i]; i++)
			list[i] = nm_utils_str_utf8safe_unescape_cp (list[i], NM_UTILS_STR_UTF8_SAFE_FLAG_NONE);
	}

	/* The returned buffer here is not at all compact. That means, it has additional
	 * memory allocations and is larger than needed. That means, you should not keep
	 * this result around, only process it further and free it. */
	return (char **) list;
}

static gboolean
no_auto_default_to_file (const char *no_auto_default_file, const char *const*no_auto_default, GError **error)
{
	nm_auto_free_gstring GString *data = NULL;
	gsize i;

	data = g_string_new ("");
	for (i = 0; no_auto_default && no_auto_default[i]; i++) {
		gs_free char *s_to_free = NULL;
		const char *s = no_auto_default[i];

		s = nm_utils_str_utf8safe_escape (s,
		                                    NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
		                                  | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII,
		                                  &s_to_free);
		g_string_append (data, s);
		g_string_append_c (data, '\n');
	}
	return  g_file_set_contents (no_auto_default_file, data->str, data->len, error);
}

gboolean
nm_config_get_no_auto_default_for_device (NMConfig *self, NMDevice *device)
{
	NMConfigPrivate *priv;

	g_return_val_if_fail (NM_IS_CONFIG (self), FALSE);

	priv = NM_CONFIG_GET_PRIVATE (self);

	if (priv->configure_and_quit == NM_CONFIG_CONFIGURE_AND_QUIT_INITRD)
		return TRUE;

	return nm_config_data_get_no_auto_default_for_device (priv->config_data, device);
}

void
nm_config_set_no_auto_default_for_device (NMConfig *self, NMDevice *device)
{
	NMConfigPrivate *priv;
	GError *error = NULL;
	NMConfigData *new_data = NULL;
	gs_free char *spec_to_free = NULL;
	const char *ifname;
	const char *hw_address;
	const char *spec;
	const char *const*no_auto_default_current;
	gs_free const char **no_auto_default_new = NULL;
	gboolean is_fake;
	gsize len;
	gssize idx;

	g_return_if_fail (NM_IS_CONFIG (self));
	g_return_if_fail (NM_IS_DEVICE (device));

	priv = NM_CONFIG_GET_PRIVATE (self);

	hw_address = nm_device_get_permanent_hw_address_full (device, TRUE, &is_fake);

	if (!hw_address) {
		/* No MAC address, not even a fake one. We don't do anything for this device. */
		return;
	}

	if (is_fake) {
		/* A fake MAC address, no point in storing it to the file.
		 * Also, nm_match_spec_device() would ignore fake MAC addresses.
		 *
		 * Instead, try the interface-name...  */
		ifname = nm_device_get_ip_iface (device);
		if (!nm_utils_ifname_valid_kernel (ifname, NULL))
			return;

		spec_to_free = g_strdup_printf (NM_MATCH_SPEC_INTERFACE_NAME_TAG"=%s", ifname);
		spec = spec_to_free;
	} else
		spec = hw_address;

	no_auto_default_current = nm_config_data_get_no_auto_default (priv->config_data);

	len = NM_PTRARRAY_LEN (no_auto_default_current);

	idx = nm_utils_ptrarray_find_binary_search ((gconstpointer *) no_auto_default_current,
	                                            len,
	                                            spec,
	                                            nm_strcmp_with_data,
	                                            NULL,
	                                            NULL,
	                                            NULL);
	if (idx >= 0) {
		/* @spec is already blocked. We don't have to update our in-memory representation.
		 * Maybe we should write to no_auto_default_file anew, but let's save that too. */
		return;
	}

	idx = ~idx;

	no_auto_default_new = g_new (const char *, len + 2);
	if (idx > 0)
		memcpy (no_auto_default_new, no_auto_default_current, sizeof (const char *) * idx);
	no_auto_default_new[idx] = spec;
	if (idx < len)
		memcpy (&no_auto_default_new[idx + 1], &no_auto_default_current[idx], sizeof (const char *) * (len - idx));
	no_auto_default_new[len + 1] = NULL;

	if (!no_auto_default_to_file (priv->no_auto_default_file, no_auto_default_new, &error)) {
		_LOGW ("Could not update no-auto-default.state file: %s",
		       error->message);
		g_error_free (error);
	}

	new_data = nm_config_data_new_update_no_auto_default (priv->config_data, no_auto_default_new);

	_set_config_data (self, new_data, NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT);
}

/*****************************************************************************/

static void
_nm_config_cmd_line_options_clear (NMConfigCmdLineOptions *cli)
{
	nm_clear_g_free (&cli->config_main_file);
	nm_clear_g_free (&cli->config_dir);
	nm_clear_g_free (&cli->system_config_dir);
	nm_clear_g_free (&cli->no_auto_default_file);
	nm_clear_g_free (&cli->intern_config_file);
	nm_clear_g_free (&cli->state_file);
	nm_clear_g_free (&cli->plugins);
	cli->configure_and_quit = NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED;
	cli->is_debug = FALSE;
	nm_clear_g_free (&cli->connectivity_uri);
	nm_clear_g_free (&cli->connectivity_response);
	cli->connectivity_interval = -1;
	cli->first_start = FALSE;
}

static void
_nm_config_cmd_line_options_copy (const NMConfigCmdLineOptions *cli, NMConfigCmdLineOptions *dst)
{
	g_return_if_fail (cli);
	g_return_if_fail (dst);
	g_return_if_fail (cli != dst);

	_nm_config_cmd_line_options_clear (dst);
	dst->config_dir = g_strdup (cli->config_dir);
	dst->system_config_dir = g_strdup (cli->system_config_dir);
	dst->config_main_file = g_strdup (cli->config_main_file);
	dst->no_auto_default_file = g_strdup (cli->no_auto_default_file);
	dst->intern_config_file = g_strdup (cli->intern_config_file);
	dst->state_file = g_strdup (cli->state_file);
	dst->plugins = g_strdup (cli->plugins);
	dst->configure_and_quit = cli->configure_and_quit;
	dst->is_debug = cli->is_debug;
	dst->connectivity_uri = g_strdup (cli->connectivity_uri);
	dst->connectivity_response = g_strdup (cli->connectivity_response);
	dst->connectivity_interval = cli->connectivity_interval;
	dst->first_start = cli->first_start;
}

NMConfigCmdLineOptions *
nm_config_cmd_line_options_new (gboolean first_start)
{
	NMConfigCmdLineOptions *cli = g_new0 (NMConfigCmdLineOptions, 1);

	_nm_config_cmd_line_options_clear (cli);

	cli->first_start = first_start;

	return cli;
}

void
nm_config_cmd_line_options_free (NMConfigCmdLineOptions *cli)
{
	g_return_if_fail (cli);

	_nm_config_cmd_line_options_clear (cli);
	g_free (cli);
}

static NMConfigConfigureAndQuitType
string_to_configure_and_quit (const char *value, GError **error)
{
	NMConfigConfigureAndQuitType ret;

	if (value == NULL)
		return NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED;

	if (nm_streq (value, "initrd"))
		return NM_CONFIG_CONFIGURE_AND_QUIT_INITRD;

	ret = nm_config_parse_boolean (value, NM_CONFIG_CONFIGURE_AND_QUIT_INVALID);
	if (ret == NM_CONFIG_CONFIGURE_AND_QUIT_INVALID)
		g_set_error (error, 1, 0, N_("'%s' is not valid"), value);

	return ret;
}

static gboolean
parse_configure_and_quit (const char *option_name, const char *value, gpointer user_data, GError **error)
{
	NMConfigCmdLineOptions *cli = user_data;

	if (value == NULL)
		cli->configure_and_quit = NM_CONFIG_CONFIGURE_AND_QUIT_ENABLED;
	else
		cli->configure_and_quit = string_to_configure_and_quit (value, error);

	if (cli->configure_and_quit == NM_CONFIG_CONFIGURE_AND_QUIT_INVALID) {
		g_prefix_error (error, N_("Bad '%s' option: "), option_name);
		return FALSE;
	}

	return TRUE;
}

void
nm_config_cmd_line_options_add_to_entries (NMConfigCmdLineOptions *cli,
                                           GOptionContext *opt_ctx)
{
	GOptionGroup *group;
	GOptionEntry config_options[] = {
		{ "config", 0, 0, G_OPTION_ARG_FILENAME, &cli->config_main_file, N_("Config file location"), DEFAULT_CONFIG_MAIN_FILE },
		{ "config-dir", 0, 0, G_OPTION_ARG_FILENAME, &cli->config_dir, N_("Config directory location"), DEFAULT_CONFIG_DIR },
		{ "system-config-dir", 0, 0, G_OPTION_ARG_FILENAME, &cli->system_config_dir, N_("System config directory location"), DEFAULT_SYSTEM_CONFIG_DIR },
		{ "intern-config", 0, 0, G_OPTION_ARG_FILENAME, &cli->intern_config_file, N_("Internal config file location"), DEFAULT_INTERN_CONFIG_FILE },
		{ "state-file", 0, 0, G_OPTION_ARG_FILENAME, &cli->state_file, N_("State file location"), DEFAULT_STATE_FILE },
		{ "no-auto-default", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME, &cli->no_auto_default_file, N_("State file for no-auto-default devices"), DEFAULT_NO_AUTO_DEFAULT_FILE },
		{ "plugins", 0, 0, G_OPTION_ARG_STRING, &cli->plugins, N_("List of plugins separated by ','"), NM_CONFIG_DEFAULT_MAIN_PLUGINS },
		{ "configure-and-quit", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, parse_configure_and_quit, N_("Quit after initial configuration"), NULL },
		{ "debug", 'd', 0, G_OPTION_ARG_NONE, &cli->is_debug, N_("Don't become a daemon, and log to stderr"), NULL },

			/* These three are hidden for now, and should eventually just go away. */
		{ "connectivity-uri", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &cli->connectivity_uri, N_("An http(s) address for checking internet connectivity"), "http://example.com" },
		{ "connectivity-interval", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_INT, &cli->connectivity_interval, N_("The interval between connectivity checks (in seconds)"), G_STRINGIFY (NM_CONFIG_DEFAULT_CONNECTIVITY_INTERVAL) },
		{ "connectivity-response", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &cli->connectivity_response, N_("The expected start of the response"), NM_CONFIG_DEFAULT_CONNECTIVITY_RESPONSE },
		{ 0 },
	};

	g_return_if_fail (opt_ctx);
	g_return_if_fail (cli);

	group = g_option_group_new ("nm", N_("NetworkManager options" ), N_("Show NetworkManager options"), cli, NULL);

	g_option_group_add_entries (group, config_options);
	g_option_context_add_group (opt_ctx, group);
}

/*****************************************************************************/

GKeyFile *
nm_config_create_keyfile ()
{
	GKeyFile *keyfile;

	keyfile = g_key_file_new ();
	g_key_file_set_list_separator (keyfile, NM_CONFIG_KEYFILE_LIST_SEPARATOR);
	return keyfile;
}

/* this is an external variable, to make loading testable. Other then that,
 * no code is supposed to change this. */
guint _nm_config_match_nm_version = NM_VERSION;
char *_nm_config_match_env = NULL;

static gboolean
ignore_config_snippet (GKeyFile *keyfile, gboolean is_base_config)
{
	GSList *specs;
	gboolean as_bool;
	NMMatchSpecMatchType match_type;

	if (is_base_config)
		return FALSE;

	if (!g_key_file_has_key (keyfile, NM_CONFIG_KEYFILE_GROUP_CONFIG, NM_CONFIG_KEYFILE_KEY_CONFIG_ENABLE, NULL))
		return FALSE;

	/* first, let's try to parse the value as plain boolean. If that is possible, we don't treat
	 * the value as match-spec. */
	as_bool = nm_config_keyfile_get_boolean (keyfile, NM_CONFIG_KEYFILE_GROUP_CONFIG, NM_CONFIG_KEYFILE_KEY_CONFIG_ENABLE, -1);
	if (as_bool != -1)
		return !as_bool;

	if (G_UNLIKELY (!_nm_config_match_env)) {
		const char *e;

		e = g_getenv ("NM_CONFIG_ENABLE_TAG");
		_nm_config_match_env = g_strdup (e ?: "");
	}

	/* second, interpret the value as match-spec. */
	specs = nm_config_get_match_spec (keyfile, NM_CONFIG_KEYFILE_GROUP_CONFIG, NM_CONFIG_KEYFILE_KEY_CONFIG_ENABLE, NULL);
	match_type = nm_match_spec_config (specs,
	                                   _nm_config_match_nm_version,
	                                   _nm_config_match_env);
	g_slist_free_full (specs, g_free);

	return match_type != NM_MATCH_SPEC_MATCH;
}

static int
_sort_groups_cmp (const char **pa, const char **pb, gpointer dummy)
{
	const char *a = *pa;
	const char *b = *pb;
	gboolean a_is_connection, b_is_connection;
	gboolean a_is_device, b_is_device;

	a_is_connection = NM_STR_HAS_PREFIX (a, NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION);
	b_is_connection = NM_STR_HAS_PREFIX (b, NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION);

	if (a_is_connection != b_is_connection) {
		/* one is a [connection*] entry, the other not. We sort [connection*] entries
		 * after.  */
		if (a_is_connection)
			return 1;
		return -1;
	}
	if (a_is_connection) {
		/* both are [connection.\+] entries. Reverse their order.
		 * One of the sections might be literally [connection]. That section
		 * is special and its order will be fixed later. It doesn't actually
		 * matter here how it compares with [connection.\+] sections. */
		return pa > pb ? -1 : 1;
	}

	a_is_device = NM_STR_HAS_PREFIX (a, NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE);
	b_is_device = NM_STR_HAS_PREFIX (b, NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE);

	if (a_is_device != b_is_device) {
		/* one is a [device*] entry, the other not. We sort [device*] entries
		 * after.  */
		if (a_is_device)
			return 1;
		return -1;
	}
	if (a_is_device) {
		/* both are [device.\+] entries. Reverse their order.
		 * One of the sections might be literally [device]. That section
		 * is special and its order will be fixed later. It doesn't actually
		 * matter here how it compares with [device.\+] sections. */
		return pa > pb ? -1 : 1;
	}

	/* don't reorder the rest. */
	return 0;
}

void
_nm_config_sort_groups (char **groups, gsize ngroups)
{
	if (ngroups > 1) {
		g_qsort_with_data (groups,
		                   ngroups,
		                   sizeof (char *),
		                   (GCompareDataFunc) _sort_groups_cmp,
		                   NULL);
	}
}

static gboolean
_setting_is_device_spec (const char *group, const char *key)
{
#define _IS(group_v, key_v) (nm_streq (group, ""group_v"") && nm_streq (key, ""key_v""))
	return    _IS (NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_NO_AUTO_DEFAULT)
	       || _IS (NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_IGNORE_CARRIER)
	       || _IS (NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_ASSUME_IPV6LL_ONLY)
	       || _IS (NM_CONFIG_KEYFILE_GROUP_KEYFILE, NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES)
	       || (NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION) && nm_streq (key, NM_CONFIG_KEYFILE_KEY_MATCH_DEVICE))
	       || (NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE    ) && nm_streq (key, NM_CONFIG_KEYFILE_KEY_MATCH_DEVICE));
}

static gboolean
_setting_is_string_list (const char *group, const char *key)
{
	return    _IS (NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_PLUGINS)
	       || _IS (NM_CONFIG_KEYFILE_GROUP_MAIN, NM_CONFIG_KEYFILE_KEY_MAIN_DEBUG)
	       || _IS (NM_CONFIG_KEYFILE_GROUP_LOGGING, NM_CONFIG_KEYFILE_KEY_LOGGING_DOMAINS)
	       || NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_TEST_APPEND_STRINGLIST);
#undef _IS
}

typedef struct {
	char *group;
	const char *const *keys;
	bool is_prefix:1;
	bool is_connection:1;
} ConfigGroup;

/* The following comment is used by check-config-options.sh, don't remove it. */
/* START OPTION LIST */

static const ConfigGroup config_groups[] = {
	{
		.group = NM_CONFIG_KEYFILE_GROUP_MAIN,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_MAIN_ASSUME_IPV6LL_ONLY,
			NM_CONFIG_KEYFILE_KEY_MAIN_AUTH_POLKIT,
			NM_CONFIG_KEYFILE_KEY_MAIN_AUTOCONNECT_RETRIES_DEFAULT,
			NM_CONFIG_KEYFILE_KEY_MAIN_CONFIGURE_AND_QUIT,
			NM_CONFIG_KEYFILE_KEY_MAIN_DEBUG,
			NM_CONFIG_KEYFILE_KEY_MAIN_DHCP,
			NM_CONFIG_KEYFILE_KEY_MAIN_DNS,
			NM_CONFIG_KEYFILE_KEY_MAIN_HOSTNAME_MODE,
			NM_CONFIG_KEYFILE_KEY_MAIN_IGNORE_CARRIER,
			NM_CONFIG_KEYFILE_KEY_MAIN_MONITOR_CONNECTION_FILES,
			NM_CONFIG_KEYFILE_KEY_MAIN_NO_AUTO_DEFAULT,
			NM_CONFIG_KEYFILE_KEY_MAIN_PLUGINS,
			NM_CONFIG_KEYFILE_KEY_MAIN_RC_MANAGER,
			NM_CONFIG_KEYFILE_KEY_MAIN_SLAVES_ORDER,
			NM_CONFIG_KEYFILE_KEY_MAIN_SYSTEMD_RESOLVED,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUP_LOGGING,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_LOGGING_AUDIT,
			NM_CONFIG_KEYFILE_KEY_LOGGING_BACKEND,
			NM_CONFIG_KEYFILE_KEY_LOGGING_DOMAINS,
			NM_CONFIG_KEYFILE_KEY_LOGGING_LEVEL,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_CONNECTIVITY_ENABLED,
			NM_CONFIG_KEYFILE_KEY_CONNECTIVITY_INTERVAL,
			NM_CONFIG_KEYFILE_KEY_CONNECTIVITY_RESPONSE,
			NM_CONFIG_KEYFILE_KEY_CONNECTIVITY_URI,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUP_KEYFILE,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_KEYFILE_HOSTNAME,
			NM_CONFIG_KEYFILE_KEY_KEYFILE_PATH,
			NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUP_IFUPDOWN,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_IFUPDOWN_MANAGED,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE,
		.is_prefix = TRUE,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT,
			NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
			NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
			NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
			NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_BACKEND,
			NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_RAND_MAC_ADDRESS,
			NM_CONFIG_KEYFILE_KEY_MATCH_DEVICE,
			NM_CONFIG_KEYFILE_KEY_STOP_MATCH,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUP_GLOBAL_DNS,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_OPTIONS,
			NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_SEARCHES,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUPPREFIX_GLOBAL_DNS_DOMAIN,
		.is_prefix = TRUE,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_DOMAIN_SERVERS,
			NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_DOMAIN_OPTIONS,
		),
	},
	{
		.group = NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION,
		.is_prefix = TRUE,
		.is_connection = TRUE,
		.keys = NM_MAKE_STRV (
			NM_CONFIG_KEYFILE_KEY_MATCH_DEVICE,
			NM_CONFIG_KEYFILE_KEY_STOP_MATCH,
		),
	},
	{ } /* sentinel */
};

/* The following comment is used by check-config-options.sh, don't remove it. */
/* END OPTION LIST */

static gboolean
check_config_key (const char *group, const char *key)
{
	const ConfigGroup *g;
	const char *const *k;
	const char **ptr;

#if NM_MORE_ASSERTS > 10
	{
		static gboolean checked = FALSE;
		const char **ptr1, **ptr2;

		/* check for duplicate elements in the static list */

		if (!checked) {
			for (ptr1 = __start_connection_defaults; ptr1 < __stop_connection_defaults; ptr1++) {
				for (ptr2 = ptr1 + 1; ptr2 < __stop_connection_defaults; ptr2++)
					nm_assert (!nm_streq (*ptr1, *ptr2));
			}
			checked = TRUE;
		}
	}
#endif

	for (g = config_groups; g->group; g++) {
		if (   (!g->is_prefix && nm_streq (group, g->group))
		    || (g->is_prefix && g_str_has_prefix (group, g->group)))
			break;
	}

	if (!g->group)
		return FALSE;

	for (k = g->keys; *k; k++) {
		if (nm_streq (key, *k))
			return TRUE;
	}

	if (g->is_connection) {
		for (ptr = __start_connection_defaults; ptr < __stop_connection_defaults; ptr++) {
			if (nm_streq (key, *ptr))
				return TRUE;
		}
		return FALSE;
	}

	return FALSE;
}

static gboolean
read_config (GKeyFile *keyfile,
             gboolean is_base_config,
             const char *dirname,
             const char *path,
             GPtrArray *warnings,
             GError **error)
{
	gs_unref_keyfile GKeyFile *kf = NULL;
	gs_strfreev char **groups = NULL;
	gs_free char *path_free = NULL;
	gsize ngroups;
	gsize nkeys;
	int g;
	int k;

	g_return_val_if_fail (keyfile, FALSE);
	g_return_val_if_fail (path, FALSE);
	g_return_val_if_fail (!error || !*error, FALSE);

	if (dirname) {
		path_free = g_build_filename (dirname, path, NULL);
		path = path_free;
	}

	if (g_file_test (path, G_FILE_TEST_EXISTS) == FALSE) {
		g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND, "file %s not found", path);
		return FALSE;
	}

	_LOGD ("Reading config file '%s'", path);

	kf = nm_config_create_keyfile ();
	if (!g_key_file_load_from_file (kf, path, G_KEY_FILE_NONE, error)) {
		g_prefix_error (error, "%s: ", path);
		return FALSE;
	}

	if (ignore_config_snippet (kf, is_base_config))
		return TRUE;

	/* the config-group is internal to every configuration snippets. It doesn't make sense
	 * to merge it into the global configuration, and it doesn't make sense to preserve the
	 * group beyond this point. */
	g_key_file_remove_group (kf, NM_CONFIG_KEYFILE_GROUP_CONFIG, NULL);

	/* Override the current settings with the new ones */
	groups = g_key_file_get_groups (kf, &ngroups);
	if (!groups)
		ngroups = 0;

	/* Within one file we reverse the order of the '[connection.\+] sections.
	 * Here we merge the current file (@kf) into @keyfile. As we merge multiple
	 * files, earlier sections (with lower priority) will be added first.
	 * But within one file, we want a top-to-bottom order. This means we
	 * must reverse the order within each file.
	 * At the very end, we will revert the order of all sections again and
	 * get thus the right behavior. This final reversing is done in
	 * NMConfigData:_get_connection_infos().  */
	_nm_config_sort_groups (groups, ngroups);

	for (g = 0; groups && groups[g]; g++) {
		const char *group = groups[g];
		gs_strfreev char **keys = NULL;

		if (NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN)) {
			/* internal groups cannot be set by user configuration. */
			continue;
		}
		keys = g_key_file_get_keys (kf, group, &nkeys, NULL);
		if (!keys)
			continue;
		for (k = 0; keys[k]; k++) {
			gs_free char *new_value = NULL;
			const char *key;
			char last_char;
			gsize key_len;

			key = keys[k];
			nm_assert (key && *key);

			if (   NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_WAS)
			    || NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_SET)) {
				/* these keys are protected. We ignore them if the user sets them. */
				continue;
			}

			if (nm_streq (key, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS)) {
				/* the "was" key is protected and it cannot be set by user configuration. */
				continue;
			}

			key_len = strlen (key);
			last_char = key[key_len - 1];
			if (   key_len > 1
			    && (last_char == '+' || last_char == '-')) {
				gs_free char *base_key = g_strndup (key, key_len - 1);
				gboolean is_string_list;
				gboolean old_val_was_set = FALSE;

				is_string_list = _setting_is_string_list (group, base_key);

				if (   is_string_list
				    || _setting_is_device_spec (group, base_key)) {
					gs_unref_ptrarray GPtrArray *new = g_ptr_array_new_with_free_func (g_free);
					char **iter_val;
					gs_strfreev  char **old_val = NULL;
					gs_free char **new_val = NULL;

					if (is_string_list) {
						gs_free_error GError *old_error = NULL;

						old_val = g_key_file_get_string_list (keyfile, group, base_key, NULL, &old_error);
						new_val = g_key_file_get_string_list (kf, group, key, NULL, NULL);
						if (   nm_streq (group, NM_CONFIG_KEYFILE_GROUP_MAIN)
						    && nm_streq (base_key, "plugins")) {
							old_val_was_set = !nm_keyfile_error_is_not_found (old_error);
							if (   !old_val
							    && !old_val_was_set) {
								/* we must fill the unspecified value with the compile-time default. */
								g_key_file_set_value (keyfile, group, base_key, NM_CONFIG_DEFAULT_MAIN_PLUGINS);
								old_val = g_key_file_get_string_list (keyfile, group, base_key, NULL, NULL);
								old_val_was_set = TRUE;
							}
						}
					} else {
						gs_free char *old_sval = nm_config_keyfile_get_value (keyfile, group, base_key, NM_CONFIG_GET_VALUE_TYPE_SPEC);
						gs_free char *new_sval = nm_config_keyfile_get_value (kf, group, key, NM_CONFIG_GET_VALUE_TYPE_SPEC);
						gs_free_slist GSList *old_specs = nm_match_spec_split (old_sval);
						gs_free_slist GSList *new_specs = nm_match_spec_split (new_sval);

						/* the key is a device spec. This is a special kind of string-list, that
						 * we must split differently. */
						old_val = _nm_utils_slist_to_strv (old_specs, FALSE);
						new_val = _nm_utils_slist_to_strv (new_specs, FALSE);
					}

					/* merge the string lists, by omitting duplicates. */

					for (iter_val = old_val; iter_val && *iter_val; iter_val++) {
						if (   last_char != '-'
						    || nm_utils_strv_find_first (new_val, -1, *iter_val) < 0)
							g_ptr_array_add (new, g_strdup (*iter_val));
					}
					for (iter_val = new_val; iter_val && *iter_val; iter_val++) {
						/* don't add duplicates. That means an "option=a,b"; "option+=a,c" results in "option=a,b,c" */
						if (   last_char == '+'
						    && nm_utils_strv_find_first (old_val, -1, *iter_val) < 0)
							g_ptr_array_add (new, *iter_val);
						else
							g_free (*iter_val);
					}

					if (new->len > 0) {
						if (is_string_list)
							nm_config_keyfile_set_string_list (keyfile, group, base_key, (const char *const*) new->pdata, new->len);
						else {
							gs_free_slist GSList *specs = NULL;
							gs_free char *specs_joined = NULL;

							g_ptr_array_add (new, NULL);
							specs = _nm_utils_strv_to_slist ((char **) new->pdata, FALSE);

							specs_joined = nm_match_spec_join (specs);

							g_key_file_set_value (keyfile, group, base_key, specs_joined);
						}
					} else {
						if (is_string_list && !old_val_was_set)
							g_key_file_remove_key (keyfile, group, base_key, NULL);
						else
							g_key_file_set_value (keyfile, group, base_key, "");
					}
				} else {
					/* For any other settings we don't support extending the option with +/-.
					 * Just drop the key. */
				}
				continue;
			}

			new_value = g_key_file_get_value (kf, group, key, NULL);
			g_key_file_set_value (keyfile, group, key, new_value);

			if (!check_config_key (group, key)) {
				g_ptr_array_add (warnings,
				                 g_strdup_printf ("unknown key '%s' in section [%s] of file '%s'",
				                                  key, group, path));
			}
		}
	}

	return TRUE;
}

static gboolean
read_base_config (GKeyFile *keyfile,
                  const char *cli_config_main_file,
                  char **out_config_main_file,
                  GPtrArray *warnings,
                  GError **error)
{
	GError *my_error = NULL;

	g_return_val_if_fail (keyfile, FALSE);
	g_return_val_if_fail (out_config_main_file && !*out_config_main_file, FALSE);
	g_return_val_if_fail (!error || !*error, FALSE);

	/* Try a user-specified config file first */
	if (cli_config_main_file) {
		/* Bad user-specific config file path is a hard error */
		if (read_config (keyfile, TRUE, NULL, cli_config_main_file, warnings, error)) {
			*out_config_main_file = g_strdup (cli_config_main_file);
			return TRUE;
		} else
			return FALSE;
	}

	/* Even though we prefer NetworkManager.conf, we need to check the
	 * old nm-system-settings.conf first to preserve compat with older
	 * setups.  In package managed systems dropping a NetworkManager.conf
	 * onto the system would make NM use it instead of nm-system-settings.conf,
	 * changing behavior during an upgrade.  We don't want that.
	 */

	/* Try deprecated nm-system-settings.conf first */
	if (read_config (keyfile, TRUE, NULL, DEFAULT_CONFIG_MAIN_FILE_OLD, warnings, &my_error)) {
		*out_config_main_file = g_strdup (DEFAULT_CONFIG_MAIN_FILE_OLD);
		return TRUE;
	}

	if (!g_error_matches (my_error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND)) {
		_LOGW ("Old default config file invalid: %s",
		       my_error->message);
	}
	g_clear_error (&my_error);

	/* Try the standard config file location next */
	if (read_config (keyfile, TRUE, NULL, DEFAULT_CONFIG_MAIN_FILE, warnings, &my_error)) {
		*out_config_main_file = g_strdup (DEFAULT_CONFIG_MAIN_FILE);
		return TRUE;
	}

	if (!g_error_matches (my_error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND)) {
		_LOGW ("Default config file invalid: %s",
		       my_error->message);
		g_propagate_error (error, my_error);
		return FALSE;
	}
	g_clear_error (&my_error);

	/* If for some reason no config file exists, use the default
	 * config file path.
	 */
	*out_config_main_file = g_strdup (DEFAULT_CONFIG_MAIN_FILE);
	_LOGI ("No config file found or given; using %s",
	       DEFAULT_CONFIG_MAIN_FILE);
	return TRUE;
}

static GPtrArray *
_get_config_dir_files (const char *config_dir)
{
	GFile *dir;
	GFileEnumerator *direnum;
	GFileInfo *info;
	GPtrArray *confs;
	const char *name;

	g_return_val_if_fail (config_dir, NULL);

	confs = g_ptr_array_new_with_free_func (g_free);
	if (!*config_dir)
		return confs;

	dir = g_file_new_for_path (config_dir);
	direnum = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, 0, NULL, NULL);
	if (direnum) {
		while ((info = g_file_enumerator_next_file (direnum, NULL, NULL))) {
			name = g_file_info_get_name (info);
			if (NM_STR_HAS_SUFFIX (name, ".conf"))
				g_ptr_array_add (confs, g_strdup (name));
			g_object_unref (info);
		}
		g_object_unref (direnum);
	}
	g_object_unref (dir);

	g_ptr_array_sort (confs, nm_strcmp_p);
	return confs;
}

static void
_confs_to_description (GString *str, const GPtrArray *confs, const char *name)
{
	guint i;

	if (!confs->len)
		return;

	for (i = 0; i < confs->len; i++) {
		if (i == 0)
			g_string_append_printf (str, " (%s: ", name);
		else
			g_string_append (str, ", ");
		g_string_append (str, confs->pdata[i]);
	}
	g_string_append (str, ")");
}

static GKeyFile *
read_entire_config (const NMConfigCmdLineOptions *cli,
                    const char *config_dir,
                    const char *system_config_dir,
                    char **out_config_main_file,
                    char **out_config_description,
                    GPtrArray *warnings,
                    GError **error)
{
	gs_unref_keyfile GKeyFile *keyfile = NULL;
	gs_unref_ptrarray GPtrArray *system_confs = NULL;
	gs_unref_ptrarray GPtrArray *confs = NULL;
	gs_unref_ptrarray GPtrArray *run_confs = NULL;
	guint i;
	gs_free char *o_config_main_file = NULL;
	const char *run_config_dir = "";

	nm_assert (config_dir);
	nm_assert (system_config_dir);
	nm_assert (!out_config_main_file || !*out_config_main_file);
	nm_assert (!out_config_description || !*out_config_description);
	nm_assert (!error || !*error);
	nm_assert (warnings);

	if (   (""RUN_CONFIG_DIR)[0] == '/'
	    && !nm_streq (RUN_CONFIG_DIR, system_config_dir)
	    && !nm_streq (RUN_CONFIG_DIR, config_dir))
		run_config_dir = RUN_CONFIG_DIR;

	/* create a default configuration file. */
	keyfile = nm_config_create_keyfile ();

	system_confs = _get_config_dir_files (system_config_dir);
	confs = _get_config_dir_files (config_dir);
	run_confs = _get_config_dir_files (run_config_dir);

	for (i = 0; i < system_confs->len; ) {
		const char *filename = system_confs->pdata[i];

		/* if a same named file exists in config_dir or run_config_dir, skip it. */
		if (nm_utils_strv_find_first ((char **) confs->pdata, confs->len, filename) >= 0 ||
		    nm_utils_strv_find_first ((char **) run_confs->pdata, run_confs->len, filename) >= 0) {
			g_ptr_array_remove_index (system_confs, i);
			continue;
		}

		if (!read_config (keyfile, FALSE, system_config_dir, filename, warnings, error))
			return NULL;
		i++;
	}

	for (i = 0; i < run_confs->len; ) {
		const char *filename = run_confs->pdata[i];

		/* if a same named file exists in config_dir, skip it. */
		if (nm_utils_strv_find_first ((char **) confs->pdata, confs->len, filename) >= 0) {
			g_ptr_array_remove_index (run_confs, i);
			continue;
		}

		if (!read_config (keyfile, FALSE, run_config_dir, filename, warnings, error))
			return NULL;
		i++;
	}

	/* First read the base config file */
	if (!read_base_config (keyfile, cli ? cli->config_main_file : NULL, &o_config_main_file, warnings, error))
		return NULL;

	g_assert (o_config_main_file);

	for (i = 0; i < confs->len; i++) {
		if (!read_config (keyfile, FALSE, config_dir, confs->pdata[i], warnings, error))
			return NULL;
	}

	/* Merge settings from command line. They overwrite everything read from
	 * config files. */

	if (cli) {
		if (cli->plugins) {
			/* plugins is a string list. Set the value directly, so the user has to do proper escaping
			 * on the command line. */
			g_key_file_set_value (keyfile, NM_CONFIG_KEYFILE_GROUP_MAIN, "plugins", cli->plugins);
		}

		switch (cli->configure_and_quit) {
		case NM_CONFIG_CONFIGURE_AND_QUIT_INVALID:
			g_assert_not_reached ();
			break;
		case NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED:
			/* do nothing */
			break;
		case NM_CONFIG_CONFIGURE_AND_QUIT_ENABLED:
			g_key_file_set_boolean (keyfile, NM_CONFIG_KEYFILE_GROUP_MAIN, "configure-and-quit", TRUE);
			break;
		case NM_CONFIG_CONFIGURE_AND_QUIT_INITRD:
			g_key_file_set_string (keyfile, NM_CONFIG_KEYFILE_GROUP_MAIN, "configure-and-quit", "initrd");
			break;
		}

		if (cli->connectivity_uri && cli->connectivity_uri[0])
			g_key_file_set_string (keyfile, NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY, "uri", cli->connectivity_uri);
		if (cli->connectivity_interval >= 0)
			g_key_file_set_integer (keyfile, NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY, "interval", cli->connectivity_interval);
		if (cli->connectivity_response && cli->connectivity_response[0])
			g_key_file_set_string (keyfile, NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY, "response", cli->connectivity_response);
	}

	if (out_config_description) {
		GString *str;

		str = g_string_new (o_config_main_file);
		_confs_to_description (str, system_confs, "lib");
		_confs_to_description (str, run_confs, "run");
		_confs_to_description (str, confs, "etc");
		*out_config_description = g_string_free (str, FALSE);
	}
	NM_SET_OUT (out_config_main_file, g_steal_pointer (&o_config_main_file));

	return g_steal_pointer (&keyfile);
}

static gboolean
_is_atomic_section (const char *const*atomic_section_prefixes, const char *group)
{
	if (atomic_section_prefixes) {
		for (; *atomic_section_prefixes; atomic_section_prefixes++) {
			if (   **atomic_section_prefixes
			    && g_str_has_prefix (group, *atomic_section_prefixes))
				return TRUE;
		}
	}
	return FALSE;
}

static void
_string_append_val (GString *str, const char *value)
{
	if (!value)
		return;
	g_string_append_c (str, '+');
	while (TRUE) {
		switch (*value) {
		case '\0':
			return;
		case '\\':
		case '+':
		case '#':
		case ':':
			g_string_append_c (str, '+');
			/* fall-through */
		default:
			g_string_append_c (str, *value);
		}
		value++;
	}
}

static char *
_keyfile_serialize_section (GKeyFile *keyfile, const char *group)
{
	gs_strfreev char **keys = NULL;
	GString *str;
	guint k;

	if (keyfile)
		keys = g_key_file_get_keys (keyfile, group, NULL, NULL);
	if (!keys)
		return g_strdup ("0#");

	/* prepend a version. */
	str = g_string_new ("1#");

	for (k = 0; keys[k]; k++) {
		const char *key = keys[k];
		gs_free char *value = NULL;

		_string_append_val (str, key);
		g_string_append_c (str, ':');

		value = g_key_file_get_value (keyfile, group, key, NULL);
		_string_append_val (str, value);
		g_string_append_c (str, '#');
	}
	return g_string_free (str, FALSE);
}

gboolean
nm_config_keyfile_has_global_dns_config (GKeyFile *keyfile, gboolean internal)
{
	gs_strfreev char **groups = NULL;
	guint g;
	const char *prefix;

	if (!keyfile)
		return FALSE;
	if (g_key_file_has_group (keyfile,
	                          internal
	                              ? NM_CONFIG_KEYFILE_GROUP_GLOBAL_DNS
	                              : NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS))
		return TRUE;

	groups = g_key_file_get_groups (keyfile, NULL);
	if (!groups)
		return FALSE;

	prefix = internal ? NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN : NM_CONFIG_KEYFILE_GROUPPREFIX_GLOBAL_DNS_DOMAIN;

	for (g = 0; groups[g]; g++) {
		if (g_str_has_prefix (groups[g], prefix))
			return TRUE;
	}
	return FALSE;
}

/**
 * intern_config_read:
 * @filename: the filename where to store the internal config
 * @keyfile_conf: the merged configuration from user (/etc/NM/NetworkManager.conf).
 * @out_needs_rewrite: (allow-none): whether the read keyfile contains inconsistent
 *   data (compared to @keyfile_conf). If %TRUE, you might want to rewrite
 *   the file.
 *
 * Does the opposite of intern_config_write(). It reads the internal configuration.
 * Note that the actual format of how the configuration is saved in @filename
 * is different then what we return here. NMConfig manages what is written internally
 * by having it inside a keyfile_intern. But we don't write that to disk as is.
 * Especially, we also store parts of @keyfile_conf as ".was" and on read we compare
 * what we have, with what ".was".
 *
 * Returns: a #GKeyFile instance with the internal configuration.
 */
static GKeyFile *
intern_config_read (const char *filename,
                    GKeyFile *keyfile_conf,
                    const char *const*atomic_section_prefixes,
                    gboolean *out_needs_rewrite)
{
	GKeyFile *keyfile_intern;
	GKeyFile *keyfile;
	gboolean needs_rewrite = FALSE;
	gs_strfreev char **groups = NULL;
	guint g, k;
	gboolean has_intern = FALSE;

	g_return_val_if_fail (filename, NULL);

	if (!*filename) {
		if (out_needs_rewrite)
			*out_needs_rewrite = FALSE;
		return NULL;
	}

	keyfile_intern = nm_config_create_keyfile ();

	keyfile = nm_config_create_keyfile ();
	if (!g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL)) {
		needs_rewrite = TRUE;
		goto out;
	}

	groups = g_key_file_get_groups (keyfile, NULL);
	for (g = 0; groups && groups[g]; g++) {
		gs_strfreev char **keys = NULL;
		const char *group = groups[g];
		gboolean is_intern, is_atomic;

		if (nm_streq (group, NM_CONFIG_KEYFILE_GROUP_CONFIG))
			continue;

		keys = g_key_file_get_keys (keyfile, group, NULL, NULL);
		if (!keys)
			continue;

		is_intern = NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN);
		is_atomic = !is_intern && _is_atomic_section (atomic_section_prefixes, group);

		if (is_atomic) {
			gs_free char *conf_section_was = NULL;
			gs_free char *conf_section_is = NULL;

			conf_section_is = _keyfile_serialize_section (keyfile_conf, group);
			conf_section_was = g_key_file_get_string (keyfile, group, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS, NULL);

			if (!nm_streq0 (conf_section_was, conf_section_is)) {
				/* the section no longer matches. Skip it entirely. */
				needs_rewrite = TRUE;
				continue;
			}
			/* we must set the "was" marker in our keyfile, so that we know that the section
			 * from user config is overwritten. The value doesn't matter, it's just a marker
			 * that this section is present. */
			g_key_file_set_value (keyfile_intern, group, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS, "");
		}

		for (k = 0; keys[k]; k++) {
			gs_free char *value_set = NULL;
			const char *key = keys[k];

			value_set = g_key_file_get_value (keyfile, group, key, NULL);

			if (is_intern) {
				has_intern = TRUE;
				g_key_file_set_value (keyfile_intern, group, key, value_set);
			} else if (is_atomic) {
				if (nm_streq (key, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS))
					continue;
				g_key_file_set_value (keyfile_intern, group, key, value_set);
			} else if (NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_SET)) {
				const char *key_base = &key[NM_STRLEN (NM_CONFIG_KEYFILE_KEYPREFIX_SET)];
				gs_free char *value_was = NULL;
				gs_free char *value_conf = NULL;
				gs_free char *key_was = g_strdup_printf (NM_CONFIG_KEYFILE_KEYPREFIX_WAS"%s", key_base);

				if (keyfile_conf)
					value_conf = g_key_file_get_value (keyfile_conf, group, key_base, NULL);
				value_was = g_key_file_get_value (keyfile, group, key_was, NULL);

				if (!nm_streq0 (value_conf, value_was)) {
					/* if value_was is no longer the same as @value_conf, it means the user
					 * changed the configuration since the last write. In this case, we
					 * drop the value. It also means our file is out-of-date, and we should
					 * rewrite it. */
					needs_rewrite = TRUE;
					continue;
				}
				has_intern = TRUE;
				g_key_file_set_value (keyfile_intern, group, key_base, value_set);
			} else if (NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_WAS)) {
				const char *key_base = &key[NM_STRLEN (NM_CONFIG_KEYFILE_KEYPREFIX_WAS)];
				gs_free char *key_set = g_strdup_printf (NM_CONFIG_KEYFILE_KEYPREFIX_SET"%s", key_base);
				gs_free char *value_was = NULL;
				gs_free char *value_conf = NULL;

				if (g_key_file_has_key (keyfile, group, key_set, NULL)) {
					/* we have a matching "set" key too. Handle the "was" key there. */
					continue;
				}

				if (keyfile_conf)
					value_conf = g_key_file_get_value (keyfile_conf, group, key_base, NULL);
				value_was = g_key_file_get_value (keyfile, group, key, NULL);

				if (!nm_streq0 (value_conf, value_was)) {
					/* if value_was is no longer the same as @value_conf, it means the user
					 * changed the configuration since the last write. In this case, we
					 * don't overwrite the user-provided value. It also means our file is
					 * out-of-date, and we should rewrite it. */
					needs_rewrite = TRUE;
					continue;
				}
				has_intern = TRUE;
				/* signal the absence of the value. That means, we must propagate the
				 * "was" key to NMConfigData, so that it knows to hide the corresponding
				 * user key. */
				g_key_file_set_value (keyfile_intern, group, key, "");
			} else
				needs_rewrite = TRUE;
		}
	}

out:
	/*
	 * If user configuration specifies global DNS options, the DNS
	 * options in internal configuration must be deleted. Otherwise, a
	 * deletion of options from user configuration may cause the
	 * internal options to appear again.
	 */
	if (nm_config_keyfile_has_global_dns_config (keyfile_conf, FALSE)) {
		if (g_key_file_remove_group (keyfile_intern, NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS, NULL))
			needs_rewrite = TRUE;
		for (g = 0; groups && groups[g]; g++) {
			if (   NM_STR_HAS_PREFIX (groups[g], NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN)
			    && groups[g][NM_STRLEN (NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN)]) {
				g_key_file_remove_group (keyfile_intern, groups[g], NULL);
				needs_rewrite = TRUE;
			}
		}
	}

	g_key_file_unref (keyfile);

	if (out_needs_rewrite)
		*out_needs_rewrite = needs_rewrite;

	_LOGD ("intern config file \"%s\"", filename);

	if (!has_intern) {
		g_key_file_unref (keyfile_intern);
		return NULL;
	}
	return keyfile_intern;
}

static int
_intern_config_write_sort_fcn (const char **a, const char **b, const char *const*atomic_section_prefixes)
{
	const char *g_a = *a;
	const char *g_b = *b;
	gboolean a_is, b_is;

	a_is = NM_STR_HAS_PREFIX (g_a, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN);
	b_is = NM_STR_HAS_PREFIX (g_b, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN);

	if (a_is != b_is) {
		if (a_is)
			return 1;
		return -1;
	}
	if (!a_is) {
		a_is = _is_atomic_section (atomic_section_prefixes, g_a);
		b_is = _is_atomic_section (atomic_section_prefixes, g_b);

		if (a_is != b_is) {
			if (a_is)
				return 1;
			return -1;
		}
	}
	return g_strcmp0 (g_a, g_b);
}

static gboolean
intern_config_write (const char *filename,
                     GKeyFile *keyfile_intern,
                     GKeyFile *keyfile_conf,
                     const char *const*atomic_section_prefixes,
                     GError **error)
{
	GKeyFile *keyfile;
	gs_strfreev char **groups = NULL;
	guint g, k;
	gboolean success = FALSE;
	GError *local = NULL;

	g_return_val_if_fail (filename, FALSE);

	if (!*filename) {
		g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND, "no filename to write (use --intern-config?)");
		return FALSE;
	}

	keyfile = nm_config_create_keyfile ();

	if (keyfile_intern) {
		groups = g_key_file_get_groups (keyfile_intern, NULL);
		if (groups && groups[0]) {
			g_qsort_with_data (groups,
			                   g_strv_length (groups),
			                   sizeof (char *),
			                   (GCompareDataFunc) _intern_config_write_sort_fcn,
			                   (gpointer) atomic_section_prefixes);
		}
	}
	for (g = 0; groups && groups[g]; g++) {
		gs_strfreev char **keys = NULL;
		const char *group = groups[g];
		gboolean is_intern, is_atomic;

		keys = g_key_file_get_keys (keyfile_intern, group, NULL, NULL);
		if (!keys)
			continue;

		is_intern = NM_STR_HAS_PREFIX (group, NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN);
		is_atomic = !is_intern && _is_atomic_section (atomic_section_prefixes, group);

		if (is_atomic) {
			if (   (!keys[0] || (!keys[1] && strcmp (keys[0], NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS) == 0))
			    && !g_key_file_has_group (keyfile_conf, group)) {
				/* we are about to save an atomic section. However, we don't have any additional
				 * keys on our own and there is no user-provided (overlapping) section either.
				 * We don't have to write an empty section (i.e. skip the useless ".was=0#"). */
				continue;
			} else {
				gs_free char *conf_section_is = NULL;

				conf_section_is = _keyfile_serialize_section (keyfile_conf, group);
				g_key_file_set_string (keyfile, group, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS, conf_section_is);
				g_key_file_set_comment (keyfile, group, NULL,
				                        " Overwrites entire section from 'NetworkManager.conf'",
				                        NULL);
			}
		}

		for (k = 0; keys[k]; k++) {
			const char *key = keys[k];
			gs_free char *value_set = NULL;
			gs_free char *key_set = NULL;

			if (   !is_intern
			    && strcmp (key, NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS) == 0) {
				g_warn_if_fail (is_atomic);
				continue;
			}

			value_set = g_key_file_get_value (keyfile_intern, group, key, NULL);

			if (is_intern || is_atomic)
				g_key_file_set_value (keyfile, group, key, value_set);
			else {
				gs_free char *value_was = NULL;

				if (NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_SET)) {
					/* Setting a key with .set prefix has no meaning, as these keys
					 * are protected. Just set the value you want to set instead.
					 * Why did this happen?? */
					g_warn_if_reached ();
				} else if (NM_STR_HAS_PREFIX_WITH_MORE (key, NM_CONFIG_KEYFILE_KEYPREFIX_WAS)) {
					const char *key_base = &key[NM_STRLEN (NM_CONFIG_KEYFILE_KEYPREFIX_WAS)];

					if (   NM_STR_HAS_PREFIX_WITH_MORE (key_base, NM_CONFIG_KEYFILE_KEYPREFIX_SET)
					    || NM_STR_HAS_PREFIX_WITH_MORE (key_base, NM_CONFIG_KEYFILE_KEYPREFIX_WAS)) {
						g_warn_if_reached ();
						continue;
					}

					if (g_key_file_has_key (keyfile_intern, group, key_base, NULL)) {
						/* There is also a matching key_base entry. Skip processing
						 * the .was. key ad handle the key_base in the other else branch. */
						continue;
					}

					if (keyfile_conf) {
						value_was = g_key_file_get_value (keyfile_conf, group, key_base, NULL);
						if (value_was)
							g_key_file_set_value (keyfile, group, key, value_was);
					}
				} else {
					if (keyfile_conf) {
						value_was = g_key_file_get_value (keyfile_conf, group, key, NULL);
						if (nm_streq0 (value_set, value_was)) {
							/* there is no point in storing the identical value as we have via
							 * user configuration. Skip it. */
							continue;
						}
						if (value_was) {
							gs_free char *key_was = NULL;

							key_was = g_strdup_printf (NM_CONFIG_KEYFILE_KEYPREFIX_WAS"%s", key);
							g_key_file_set_value (keyfile, group, key_was, value_was);
						}
					}
					key = key_set = g_strdup_printf (NM_CONFIG_KEYFILE_KEYPREFIX_SET"%s", key);
					g_key_file_set_value (keyfile, group, key, value_set);
				}
			}
		}
		if (   is_intern
		    && g_key_file_has_group (keyfile, group)) {
			g_key_file_set_comment (keyfile, group, NULL,
			                        " Internal section. Not overwritable via user configuration in 'NetworkManager.conf'",
			                        NULL);
		}
	}

	g_key_file_set_comment (keyfile, NULL, NULL,
	                        " Internal configuration file. This file is written and read\n"
	                        " by NetworkManager and its configuration values are merged\n"
	                        " with the configuration from 'NetworkManager.conf'.\n"
	                        "\n"
	                        " Keys with a \""NM_CONFIG_KEYFILE_KEYPREFIX_SET"\" prefix specify the value to set.\n"
	                        " A corresponding key with a \""NM_CONFIG_KEYFILE_KEYPREFIX_WAS"\" prefix records the value\n"
	                        " of the user configuration at the time of storing the file.\n"
	                        " The value from internal configuration is rejected if the corresponding\n"
	                        " \""NM_CONFIG_KEYFILE_KEYPREFIX_WAS"\" key no longer matches the configuration from 'NetworkManager.conf'.\n"
	                        " That means, if you modify a value in 'NetworkManager.conf', the internal\n"
	                        " overwrite no longer matches and is ignored.\n"
	                        "\n"
	                        " Certain sections can only be overwritten whole, not on a per key basis.\n"
	                        " Such sections are marked with a \""NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS"\" key that records the user configuration\n"
	                        " at the time of writing.\n"
	                        "\n"
	                        " Internal sections of the form [" NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN "*] cannot\n"
	                        " be set by user configuration.\n"
	                        "\n"
	                        " CHANGES TO THIS FILE WILL BE OVERWRITTEN",
	                        NULL);

	success = g_key_file_save_to_file (keyfile, filename, &local);

	_LOGD ("write intern config file \"%s\"%s%s", filename, success ? "" : ": ", success ? "" : local->message);
	g_key_file_unref (keyfile);
	if (!success)
		g_propagate_error (error, local);
	return success;
}

/*****************************************************************************/

GSList *
nm_config_get_match_spec (const GKeyFile *keyfile, const char *group, const char *key, gboolean *out_has_key)
{
	gs_free char *value = NULL;

	/* nm_match_spec_split() already supports full escaping and is basically
	 * a modified version of g_key_file_parse_value_as_string(). So we first read
	 * the raw value (g_key_file_get_value()), and do the parsing ourselves. */
	value = g_key_file_get_value ((GKeyFile *) keyfile, group, key, NULL);
	if (out_has_key)
		*out_has_key = !!value;
	return nm_match_spec_split (value);
}

/*****************************************************************************/

gboolean
nm_config_set_global_dns (NMConfig *self, NMGlobalDnsConfig *global_dns, GError **error)
{
	NMConfigPrivate *priv;
	GKeyFile *keyfile;
	char **groups;
	const NMGlobalDnsConfig *old_global_dns;
	guint i;

	g_return_val_if_fail (NM_IS_CONFIG (self), FALSE);

	priv = NM_CONFIG_GET_PRIVATE (self);
	g_return_val_if_fail (priv->config_data, FALSE);

	old_global_dns = nm_config_data_get_global_dns_config (priv->config_data);
	if (old_global_dns && !nm_global_dns_config_is_internal (old_global_dns)) {
		g_set_error_literal (error, 1, 0,
		                     "Global DNS configuration already set via configuration file");
		return FALSE;
	}

	keyfile = nm_config_data_clone_keyfile_intern (priv->config_data);

	/* Remove existing groups */
	g_key_file_remove_group (keyfile, NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS, NULL);
	groups = g_key_file_get_groups (keyfile, NULL);
	for (i = 0; groups[i]; i++) {
		if (NM_STR_HAS_PREFIX (groups[i], NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN))
			g_key_file_remove_group (keyfile, groups[i], NULL);
	}
	g_strfreev (groups);

	/* An empty configuration removes everything from internal configuration file */
	if (nm_global_dns_config_is_empty (global_dns))
		goto done;

	/* Set new values */
	nm_config_keyfile_set_string_list (keyfile, NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS,
	                                   NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_SEARCHES,
	                                   nm_global_dns_config_get_searches (global_dns),
	                                   -1);

	nm_config_keyfile_set_string_list (keyfile, NM_CONFIG_KEYFILE_GROUP_INTERN_GLOBAL_DNS,
	                                   NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_OPTIONS,
	                                   nm_global_dns_config_get_options (global_dns),
	                                   -1);

	for (i = 0; i < nm_global_dns_config_get_num_domains (global_dns); i++) {
		NMGlobalDnsDomain *domain = nm_global_dns_config_get_domain (global_dns, i);
		gs_free char *group_name = NULL;

		group_name = g_strdup_printf (NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN "%s",
		                              nm_global_dns_domain_get_name (domain));

		nm_config_keyfile_set_string_list (keyfile, group_name, NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_DOMAIN_SERVERS,
		                                   nm_global_dns_domain_get_servers (domain), -1);
		nm_config_keyfile_set_string_list (keyfile, group_name, NM_CONFIG_KEYFILE_KEY_GLOBAL_DNS_DOMAIN_OPTIONS,
		                                   nm_global_dns_domain_get_options (domain), -1);
	}

done:
	nm_config_set_values (self, keyfile, TRUE, FALSE);
	g_key_file_unref (keyfile);

	return TRUE;
}

/*****************************************************************************/

void nm_config_set_connectivity_check_enabled (NMConfig *self,
                                               gboolean enabled)
{
	NMConfigPrivate *priv;
	GKeyFile *keyfile;

	g_return_if_fail (NM_IS_CONFIG (self));

	priv = NM_CONFIG_GET_PRIVATE (self);
	g_return_if_fail (priv->config_data);

	keyfile = nm_config_data_clone_keyfile_intern (priv->config_data);

	/* Remove existing groups */
	g_key_file_remove_group (keyfile, NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY, NULL);

	g_key_file_set_value (keyfile, NM_CONFIG_KEYFILE_GROUP_CONNECTIVITY,
	                      "enabled", enabled ? "true" : "false");

	nm_config_set_values (self, keyfile, TRUE, FALSE);
	g_key_file_unref (keyfile);
}

/**
 * nm_config_set_values:
 * @self: the NMConfig instance
 * @keyfile_intern_new: (allow-none): the new internal settings to set.
 *   If %NULL, it is equal to an empty keyfile.
 * @allow_write: only if %TRUE, allow writing the changes to file. Otherwise,
 *   do the changes in-memory only.
 * @force_rewrite: if @allow_write is %FALSE, this has no effect. If %FALSE,
 *   only write the configuration to file, if there are any actual changes.
 *   If %TRUE, always write the configuration to file, even if tere are seemingly
 *   no changes.
 *
 *  This is the most flexible function to set values. It all depends on the
 *  keys and values you set in @keyfile_intern_new. You basically reset all
 *  internal configuration values to what is in @keyfile_intern_new.
 *
 *  There are 3 types of settings:
 *    - all groups/sections with a prefix [.intern.*] are taken as is. As these
 *      groups are separate from user configuration, there is no conflict. You set
 *      them, that's it.
 *    - there are atomic sections, i.e. sections whose name start with one of
 *      NM_CONFIG_ATOMIC_SECTION_PREFIXES. If you put values in these sections,
 *      it means you completely replace the section from user configuration.
 *      You can also hide a user provided section by only putting the special
 *      key NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS into that section.
 *    - otherwise you can overwrite individual values from user-configuration.
 *      Just set the value. Keys with a prefix NM_CONFIG_KEYFILE_KEYPREFIX_*
 *      are protected -- as they are not value user keys.
 *      You can also hide a certain user setting by putting only a key
 *      NM_CONFIG_KEYFILE_KEYPREFIX_WAS"keyname" into the keyfile.
 */
void
nm_config_set_values (NMConfig *self,
                      GKeyFile *keyfile_intern_new,
                      gboolean allow_write,
                      gboolean force_rewrite)
{
	NMConfigPrivate *priv;
	GKeyFile *keyfile_intern_current;
	GKeyFile *keyfile_user;
	GKeyFile *keyfile_new;
	GError *local = NULL;
	NMConfigData *new_data = NULL;
	gs_strfreev char **groups = NULL;
	int g;

	g_return_if_fail (NM_IS_CONFIG (self));

	priv = NM_CONFIG_GET_PRIVATE (self);

	keyfile_intern_current = _nm_config_data_get_keyfile_intern (priv->config_data);

	keyfile_new = nm_config_create_keyfile ();
	if (keyfile_intern_new)
		_nm_keyfile_copy (keyfile_new, keyfile_intern_new);

	/* ensure that every atomic section has a .was entry. */
	groups = g_key_file_get_groups (keyfile_new, NULL);
	for (g = 0; groups && groups[g]; g++) {
		if (_is_atomic_section ((const char *const*) priv->atomic_section_prefixes, groups[g]))
			g_key_file_set_value (keyfile_new, groups[g], NM_CONFIG_KEYFILE_KEY_ATOMIC_SECTION_WAS, "");
	}

	if (!_nm_keyfile_equals (keyfile_intern_current, keyfile_new, TRUE))
		new_data = nm_config_data_new_update_keyfile_intern (priv->config_data, keyfile_new);

	_LOGD ("set values(): %s", new_data ? "has changes" : "no changes");

	if (allow_write
	    && (new_data || force_rewrite)) {
		/* We write the internal config file based on the user configuration from
		 * the last load/reload. That is correct, because the intern properties might
		 * be in accordance to what NM thinks is currently configured. Even if the files
		 * on disk changed in the meantime.
		 * But if they changed, on the next reload with might throw away our just
		 * written data. That is correct, because from NM's point of view, those
		 * changes on disk happened in any case *after* now. */
		if (*priv->intern_config_file) {
			keyfile_user = _nm_config_data_get_keyfile_user (priv->config_data);
			if (!intern_config_write (priv->intern_config_file, keyfile_new, keyfile_user,
			                          (const char *const*) priv->atomic_section_prefixes, &local)) {
				_LOGW ("error saving internal configuration \"%s\": %s", priv->intern_config_file, local->message);
				g_clear_error (&local);
			}
		} else
			_LOGD ("don't persist internal configuration (no file set, use --intern-config?)");
	}
	if (new_data)
		_set_config_data (self, new_data, NM_CONFIG_CHANGE_CAUSE_SET_VALUES);

	g_key_file_unref (keyfile_new);
}

/******************************************************************************
 * State
 ******************************************************************************/

static const char *
state_get_filename (const NMConfigCmdLineOptions *cli)
{
	/* For an empty filename, we assume the user wants to disable
	 * state. NMConfig will not try to read it nor write it out. */
	if (!cli->state_file)
		return DEFAULT_STATE_FILE;
	return cli->state_file[0] ? cli->state_file : NULL;
}

static State *
state_new (void)
{
	State *state;

	state = g_slice_new0 (State);
	state->p.net_enabled = TRUE;
	state->p.wifi_enabled = TRUE;
	state->p.wwan_enabled = TRUE;

	return state;
}

static void
state_free (State *state)
{
	if (!state)
		return;
	g_slice_free (State, state);
}

static State *
state_new_from_file (const char *filename)
{
	GKeyFile *keyfile;
	gs_free_error GError *error = NULL;
	State *state;

	state = state_new ();

	if (!filename)
		return state;

	keyfile = g_key_file_new ();
	g_key_file_set_list_separator (keyfile, ',');
	if (!g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, &error)) {
		if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
			_LOGD ("state: missing state file \"%s\": %s", filename, error->message);
		else
			_LOGW ("state: error reading state file \"%s\": %s", filename, error->message);
		goto out;
	}

	_LOGD ("state: successfully read state file \"%s\"", filename);

	state->p.net_enabled  = nm_config_keyfile_get_boolean (keyfile, "main", "NetworkingEnabled", state->p.net_enabled);
	state->p.wifi_enabled = nm_config_keyfile_get_boolean (keyfile, "main", "WirelessEnabled", state->p.wifi_enabled);
	state->p.wwan_enabled = nm_config_keyfile_get_boolean (keyfile, "main", "WWANEnabled", state->p.wwan_enabled);

out:
	g_key_file_unref (keyfile);
	return state;
}

const NMConfigState *
nm_config_state_get (NMConfig *self)
{
	NMConfigPrivate *priv;

	g_return_val_if_fail (NM_IS_CONFIG (self), NULL);

	priv = NM_CONFIG_GET_PRIVATE (self);

	if (G_UNLIKELY (!priv->state)) {
		/* read the state from file lazy on first access. The reason is that
		 * we want to log a failure to read the file via nm-logging.
		 *
		 * So we cannot read the state during construction of NMConfig,
		 * because at that time nm-logging is not yet configured.
		 */
		priv->state = state_new_from_file (state_get_filename (&priv->cli));
	}

	return &priv->state->p;
}

static void
state_write (NMConfig *self)
{
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (self);
	const char *filename;
	GString *str;
	GError *error = NULL;

	if (priv->configure_and_quit != NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED)
		return;

	filename = state_get_filename (&priv->cli);

	if (!filename) {
		priv->state->p.dirty = FALSE;
		return;
	}

	str = g_string_sized_new (256);

	/* Let's construct the keyfile data by hand. */

	g_string_append (str, "[main]\n");
	g_string_append_printf (str, "NetworkingEnabled=%s\n", priv->state->p.net_enabled ? "true" : "false");
	g_string_append_printf (str, "WirelessEnabled=%s\n", priv->state->p.wifi_enabled ? "true" : "false");
	g_string_append_printf (str, "WWANEnabled=%s\n", priv->state->p.wwan_enabled ? "true" : "false");

	if (!g_file_set_contents (filename,
	                          str->str, str->len,
	                          &error)) {
		_LOGD ("state: error writing state file \"%s\": %s", filename, error->message);
		g_clear_error (&error);
		/* we leave the state dirty. That potentially means, that we try to
		 * write the file over and over again, although it isn't possible. */
		priv->state->p.dirty = TRUE;
	} else
		priv->state->p.dirty = FALSE;

	_LOGT ("state: success writing state file \"%s\"", filename);

	g_string_free (str, TRUE);
}

void
_nm_config_state_set (NMConfig *self,
                      gboolean allow_persist,
                      gboolean force_persist,
                      ...)
{
	NMConfigPrivate *priv;
	va_list ap;
	NMConfigRunStatePropertyType property_type;

	g_return_if_fail (NM_IS_CONFIG (self));

	priv = NM_CONFIG_GET_PRIVATE (self);

	va_start (ap, force_persist);

	/* We expect that the NMConfigRunStatePropertyType is an integer type <= sizeof (int).
	 * Smaller would be fine, since the variadic arguments get promoted to int.
	 * Larger would be a problem, also, because we want that "0" is a valid sentinel. */
	G_STATIC_ASSERT_EXPR (sizeof (NMConfigRunStatePropertyType) <= sizeof (int));

	while ((property_type = va_arg (ap, int)) != NM_CONFIG_STATE_PROPERTY_NONE) {
		bool *p_bool, v_bool;

		switch (property_type) {
		case NM_CONFIG_STATE_PROPERTY_NETWORKING_ENABLED:
			p_bool = &priv->state->p.net_enabled;
			break;
		case NM_CONFIG_STATE_PROPERTY_WIFI_ENABLED:
			p_bool = &priv->state->p.wifi_enabled;
			break;
		case NM_CONFIG_STATE_PROPERTY_WWAN_ENABLED:
			p_bool = &priv->state->p.wwan_enabled;
			break;
		default:
			va_end (ap);
			g_return_if_reached ();
		}

		v_bool = va_arg (ap, gboolean);
		if (*p_bool == v_bool)
			continue;
		*p_bool = v_bool;
		priv->state->p.dirty = TRUE;
	}

	va_end (ap);

	if (   allow_persist
	    && (force_persist || priv->state->p.dirty))
		state_write (self);
}

/*****************************************************************************/

#define DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE                   "device"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_MANAGED             "managed"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_PERM_HW_ADDR_FAKE   "perm-hw-addr-fake"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_CONNECTION_UUID     "connection-uuid"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NM_OWNED            "nm-owned"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_ASPIRED   "route-metric-default-aspired"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE "route-metric-default-effective"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH           "root-path"
#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER         "next-server"

static
NM_UTILS_LOOKUP_STR_DEFINE (_device_state_managed_type_to_str, NMConfigDeviceStateManagedType,
	NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT ("unknown"),
	NM_UTILS_LOOKUP_STR_ITEM (NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNKNOWN,   "unknown"),
	NM_UTILS_LOOKUP_STR_ITEM (NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNMANAGED, "unmanaged"),
	NM_UTILS_LOOKUP_STR_ITEM (NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_MANAGED,   "managed"),
);

static NMConfigDeviceStateData *
_config_device_state_data_new (int ifindex, GKeyFile *kf)
{
	NMConfigDeviceStateData *device_state;
	NMConfigDeviceStateManagedType managed_type = NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNKNOWN;
	gs_free char *connection_uuid = NULL;
	gs_free char *perm_hw_addr_fake = NULL;
	gsize connection_uuid_len;
	gsize perm_hw_addr_fake_len;
	NMTernary nm_owned;
	char *p;
	guint32 route_metric_default_effective;
	guint32 route_metric_default_aspired;

	nm_assert (kf);
	nm_assert (ifindex > 0);

	switch (nm_config_keyfile_get_boolean (kf,
	                                       DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
	                                       DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_MANAGED,
	                                       -1)) {
	case TRUE:
		managed_type = NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_MANAGED;
		connection_uuid = nm_config_keyfile_get_value (kf,
		                                               DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                                               DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_CONNECTION_UUID,
		                                               NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
		break;
	case FALSE:
		managed_type = NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNMANAGED;
		break;
	case -1:
		/* missing property in keyfile. */
		break;
	}

	perm_hw_addr_fake = nm_config_keyfile_get_value (kf,
	                                                 DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
	                                                 DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_PERM_HW_ADDR_FAKE,
	                                                 NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
	if (perm_hw_addr_fake) {
		char *normalized;

		normalized = nm_utils_hwaddr_canonical (perm_hw_addr_fake, -1);
		g_free (perm_hw_addr_fake);
		perm_hw_addr_fake = normalized;
	}

	nm_owned = nm_config_keyfile_get_boolean (kf,
	                                          DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
	                                          DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NM_OWNED,
	                                          NM_TERNARY_DEFAULT);

	/* metric zero is not a valid metric. While zero valid for IPv4, for IPv6 it is an alias
	 * for 1024. Since we handle here IPv4 and IPv6 the same, we cannot allow zero. */
	route_metric_default_effective = nm_config_keyfile_get_int64 (kf,
	                                                              DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
	                                                              DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE,
	                                                              10, 1, G_MAXUINT32, 0);
	if (route_metric_default_effective) {
		route_metric_default_aspired = nm_config_keyfile_get_int64 (kf,
		                                                            DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                                                            DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE,
		                                                            10, 1, route_metric_default_effective,
		                                                            route_metric_default_effective);
	} else
		route_metric_default_aspired = 0;

	connection_uuid_len = connection_uuid ? strlen (connection_uuid) + 1 : 0;
	perm_hw_addr_fake_len = perm_hw_addr_fake ? strlen (perm_hw_addr_fake) + 1 : 0;

	device_state = g_malloc (sizeof (NMConfigDeviceStateData) +
	                         connection_uuid_len +
	                         perm_hw_addr_fake_len);

	device_state->ifindex = ifindex;
	device_state->managed = managed_type;
	device_state->connection_uuid = NULL;
	device_state->perm_hw_addr_fake = NULL;
	device_state->nm_owned = nm_owned;
	device_state->route_metric_default_aspired = route_metric_default_aspired;
	device_state->route_metric_default_effective = route_metric_default_effective;

	p = (char *) (&device_state[1]);
	if (connection_uuid) {
		memcpy (p, connection_uuid, connection_uuid_len);
		device_state->connection_uuid = p;
		p += connection_uuid_len;
	}
	if (perm_hw_addr_fake) {
		memcpy (p, perm_hw_addr_fake, perm_hw_addr_fake_len);
		device_state->perm_hw_addr_fake = p;
		p += perm_hw_addr_fake_len;
	}

	return device_state;
}

#define DEVICE_STATE_FILENAME_LEN_MAX 60

/**
 * nm_config_device_state_load:
 * @ifindex: the ifindex for which the state is to load
 *
 * Returns: (transfer full): a run state object.
 *   Must be freed with g_free().
 */
NMConfigDeviceStateData *
nm_config_device_state_load (int ifindex)
{
	NMConfigDeviceStateData *device_state;
	char path[NM_STRLEN (NM_CONFIG_DEVICE_STATE_DIR"/") + DEVICE_STATE_FILENAME_LEN_MAX + 1];
	gs_unref_keyfile GKeyFile *kf = NULL;
	const char *nm_owned_str;

	g_return_val_if_fail (ifindex > 0, NULL);

	nm_sprintf_buf (path, "%s/%d", NM_CONFIG_DEVICE_STATE_DIR, ifindex);

	kf = nm_config_create_keyfile ();
	if (!g_key_file_load_from_file (kf, path, G_KEY_FILE_NONE, NULL))
		return NULL;

	device_state = _config_device_state_data_new (ifindex, kf);
	nm_owned_str =   device_state->nm_owned == NM_TERNARY_TRUE
	               ? ", nm-owned=1"
	               : (  device_state->nm_owned == NM_TERNARY_FALSE
	                  ? ", nm-owned=0"
	                  : "");

	_LOGT ("device-state: %s #%d (%s); managed=%s%s%s%s%s%s%s%s, route-metric-default=%"G_GUINT32_FORMAT"-%"G_GUINT32_FORMAT"",
	       kf ? "read" : "miss",
	       ifindex, path,
	       _device_state_managed_type_to_str (device_state->managed),
	       NM_PRINT_FMT_QUOTED (device_state->connection_uuid, ", connection-uuid=", device_state->connection_uuid, "", ""),
	       NM_PRINT_FMT_QUOTED (device_state->perm_hw_addr_fake, ", perm-hw-addr-fake=", device_state->perm_hw_addr_fake, "", ""),
	       nm_owned_str,
	       device_state->route_metric_default_aspired,
	       device_state->route_metric_default_effective);

	return device_state;
}

static int
_device_state_parse_filename (const char *filename)
{
	if (!filename || !filename[0])
		return 0;
	if (!NM_STRCHAR_ALL (filename, ch, g_ascii_isdigit (ch)))
		return 0;
	return _nm_utils_ascii_str_to_int64 (filename, 10, 1, G_MAXINT, 0);
}

GHashTable *
nm_config_device_state_load_all (void)
{
	GHashTable *states;
	GDir *dir;
	const char *fn;
	int ifindex;

	states = g_hash_table_new_full (nm_direct_hash, NULL, NULL, g_free);

	dir = g_dir_open (NM_CONFIG_DEVICE_STATE_DIR, 0, NULL);
	if (!dir)
		return states;

	while ((fn = g_dir_read_name (dir))) {
		NMConfigDeviceStateData *state;

		ifindex = _device_state_parse_filename (fn);
		if (ifindex <= 0)
			continue;

		state = nm_config_device_state_load (ifindex);
		if (!state)
			continue;

		if (!g_hash_table_insert (states, GINT_TO_POINTER (ifindex), state))
			nm_assert_not_reached ();
	}
	g_dir_close (dir);

	return states;
}

gboolean
nm_config_device_state_write (int ifindex,
                              NMConfigDeviceStateManagedType managed,
                              const char *perm_hw_addr_fake,
                              const char *connection_uuid,
                              NMTernary nm_owned,
                              guint32 route_metric_default_aspired,
                              guint32 route_metric_default_effective,
                              const char *next_server,
                              const char *root_path)
{
	char path[NM_STRLEN (NM_CONFIG_DEVICE_STATE_DIR"/") + DEVICE_STATE_FILENAME_LEN_MAX + 1];
	GError *local = NULL;
	gs_unref_keyfile GKeyFile *kf = NULL;

	g_return_val_if_fail (ifindex > 0, FALSE);
	g_return_val_if_fail (!connection_uuid || *connection_uuid, FALSE);
	g_return_val_if_fail (managed == NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_MANAGED || !connection_uuid, FALSE);

	nm_assert (!perm_hw_addr_fake || nm_utils_hwaddr_valid (perm_hw_addr_fake, -1));

	nm_sprintf_buf (path, "%s/%d", NM_CONFIG_DEVICE_STATE_DIR, ifindex);

	kf = nm_config_create_keyfile ();
	if (NM_IN_SET (managed,
	               NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_MANAGED,
	               NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNMANAGED)) {
		g_key_file_set_boolean (kf,
		                        DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                        DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_MANAGED,
		                        managed == NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_MANAGED);
	}
	if (perm_hw_addr_fake) {
		g_key_file_set_string (kf,
		                       DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                       DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_PERM_HW_ADDR_FAKE,
		                       perm_hw_addr_fake);
	}
	if (connection_uuid) {
		g_key_file_set_string (kf,
		                       DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                       DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_CONNECTION_UUID,
		                       connection_uuid);
	}
	if (nm_owned != NM_TERNARY_DEFAULT) {
		g_key_file_set_boolean (kf,
		                        DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                        DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NM_OWNED,
		                        nm_owned);
	}

	if (route_metric_default_effective != 0) {
		g_key_file_set_int64 (kf,
		                      DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                      DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE,
		                      route_metric_default_effective);
		if (route_metric_default_aspired != route_metric_default_effective) {
			g_key_file_set_int64 (kf,
			                      DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
			                      DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_ASPIRED,
			                      route_metric_default_aspired);
		}
	}
	if (next_server) {
		g_key_file_set_string (kf,
		                       DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                       DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER,
		                       next_server);
	}
	if (root_path) {
		g_key_file_set_string (kf,
		                       DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
		                       DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH,
		                       root_path);
	}

	if (!g_key_file_save_to_file (kf, path, &local)) {
		_LOGW ("device-state: write #%d (%s) failed: %s", ifindex, path, local->message);
		g_error_free (local);
		return FALSE;
	}
	_LOGT ("device-state: write #%d (%s); managed=%s%s%s%s%s%s%s, route-metric-default=%"G_GUINT32_FORMAT"-%"G_GUINT32_FORMAT"%s%s%s%s%s%s",
	       ifindex, path,
	       _device_state_managed_type_to_str (managed),
	       NM_PRINT_FMT_QUOTED (connection_uuid, ", connection-uuid=", connection_uuid, "", ""),
	       NM_PRINT_FMT_QUOTED (perm_hw_addr_fake, ", perm-hw-addr-fake=", perm_hw_addr_fake, "", ""),
	       route_metric_default_aspired,
	       route_metric_default_effective,
	       NM_PRINT_FMT_QUOTED (next_server, ", next-server=", next_server, "", ""),
	       NM_PRINT_FMT_QUOTED (root_path, ", root-path=", root_path, "", ""));
	return TRUE;
}

void
nm_config_device_state_prune_stale (GHashTable *preserve_ifindexes,
                                    NMPlatform *preserve_in_platform)
{
	GDir *dir;
	const char *fn;
	char buf[NM_STRLEN (NM_CONFIG_DEVICE_STATE_DIR"/") + DEVICE_STATE_FILENAME_LEN_MAX + 1] = NM_CONFIG_DEVICE_STATE_DIR"/";
	char *buf_p = &buf[NM_STRLEN (NM_CONFIG_DEVICE_STATE_DIR"/")];

	dir = g_dir_open (NM_CONFIG_DEVICE_STATE_DIR, 0, NULL);
	if (!dir)
		return;

	while ((fn = g_dir_read_name (dir))) {
		int ifindex;
		gsize fn_len;

		ifindex = _device_state_parse_filename (fn);
		if (ifindex <= 0)
			continue;

		if (   preserve_ifindexes
		    && g_hash_table_contains (preserve_ifindexes, GINT_TO_POINTER (ifindex)))
			continue;

		if (   preserve_in_platform
		    && nm_platform_link_get (preserve_in_platform, ifindex))
			continue;

		fn_len = strlen (fn);
		nm_assert (fn_len > 0);
		nm_assert (&buf_p[fn_len] < &buf[G_N_ELEMENTS (buf)]);
		memcpy (buf_p, fn, fn_len + 1u);
		nm_assert (({
		                char bb[30];

		                nm_streq0 (nm_sprintf_buf (bb, "%d", ifindex),
		                           buf_p);
		           }));
		_LOGT ("device-state: prune #%d (%s)", ifindex, buf);
		(void) unlink (buf);
	}

	g_dir_close (dir);
}

/*****************************************************************************/

static GHashTable *
_device_state_get_all (NMConfig *self)
{
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (self);

	if (G_UNLIKELY (!priv->device_states))
		priv->device_states = nm_config_device_state_load_all ();
	return priv->device_states;
}

/**
 * nm_config_device_state_get_all:
 * @self: the #NMConfig
 *
 * This function exists to give convenient access to all
 * device states. Do not ever try to modify the returned
 * hash, it's supposed to be immutable.
 *
 * Returns: the internal #GHashTable object with all device states.
 */
const GHashTable *
nm_config_device_state_get_all (NMConfig *self)
{
	g_return_val_if_fail (NM_IS_CONFIG (self), NULL);

	return _device_state_get_all (self);
}

const NMConfigDeviceStateData *
nm_config_device_state_get (NMConfig *self,
                            int ifindex)
{
	g_return_val_if_fail (NM_IS_CONFIG (self), NULL);
	g_return_val_if_fail (ifindex > 0 , NULL);

	return g_hash_table_lookup (_device_state_get_all (self), GINT_TO_POINTER (ifindex));
}

/*****************************************************************************/

void
nm_config_reload (NMConfig *self, NMConfigChangeFlags reload_flags, gboolean emit_warnings)
{
	NMConfigPrivate *priv;
	GError *error = NULL;
	GKeyFile *keyfile, *keyfile_intern;
	NMConfigData *new_data = NULL;
	char *config_main_file = NULL;
	char *config_description = NULL;
	gs_strfreev char **no_auto_default = NULL;
	gboolean intern_config_needs_rewrite;
	gs_unref_ptrarray GPtrArray *warnings = NULL;
	guint i;

	g_return_if_fail (NM_IS_CONFIG (self));
	g_return_if_fail (   reload_flags
	                  && !NM_FLAGS_ANY (reload_flags, ~NM_CONFIG_CHANGE_CAUSES)
	                  && !NM_FLAGS_ANY (reload_flags,   NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT
	                                                  | NM_CONFIG_CHANGE_CAUSE_SET_VALUES));

	priv = NM_CONFIG_GET_PRIVATE (self);

	if (!NM_FLAGS_ANY (reload_flags, NM_CONFIG_CHANGE_CAUSE_SIGHUP | NM_CONFIG_CHANGE_CAUSE_CONF)) {
		/* unless SIGHUP is specified, we don't reload the configuration from disc. */
		_set_config_data (self, NULL, reload_flags);
		return;
	}

	warnings = g_ptr_array_new_with_free_func (g_free);

	/* pass on the original command line options. This means, that
	 * options specified at command line cannot ever be reloaded from
	 * file. That seems desirable.
	 */
	keyfile = read_entire_config (&priv->cli,
	                              priv->config_dir,
	                              priv->system_config_dir,
	                              &config_main_file,
	                              &config_description,
	                              warnings,
	                              &error);
	if (!keyfile) {
		_LOGE ("Failed to reload the configuration: %s", error->message);
		g_clear_error (&error);
		_set_config_data (self, NULL, reload_flags);
		return;
	}

	no_auto_default = no_auto_default_from_file (priv->no_auto_default_file);

	keyfile_intern = intern_config_read (priv->intern_config_file,
	                                     keyfile,
	                                     (const char *const*) priv->atomic_section_prefixes,
	                                     &intern_config_needs_rewrite);
	if (intern_config_needs_rewrite) {
		intern_config_write (priv->intern_config_file, keyfile_intern, keyfile,
		                     (const char *const*) priv->atomic_section_prefixes, NULL);
	}

	new_data = nm_config_data_new (config_main_file,
	                               config_description,
	                               (const char *const*) no_auto_default,
	                               keyfile,
	                               keyfile_intern);

	if (emit_warnings) {
		nm_config_data_get_warnings (priv->config_data_orig, warnings);
		for (i = 0; i < warnings->len; i++)
			_LOGW ("%s", (const char *) warnings->pdata[i]);
	}

	g_free (config_main_file);
	g_free (config_description);
	g_key_file_unref (keyfile);
	if (keyfile_intern)
		g_key_file_unref (keyfile_intern);

	_set_config_data (self, new_data, reload_flags);
}

NM_UTILS_FLAGS2STR_DEFINE (nm_config_change_flags_to_string, NMConfigChangeFlags,

	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_CONF, "CONF"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_DNS_RC, "DNS_RC"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_DNS_FULL, "DNS_FULL"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_SIGHUP, "SIGHUP"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_SIGUSR1, "SIGUSR1"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_SIGUSR2, "SIGUSR2"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT, "NO_AUTO_DEFAULT"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CAUSE_SET_VALUES, "SET_VALUES"),

	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CONFIG_FILES, "config-files"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_VALUES, "values"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_VALUES_USER, "values-user"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_VALUES_INTERN, "values-intern"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_CONNECTIVITY, "connectivity"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_NO_AUTO_DEFAULT, "no-auto-default"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_DNS_MODE, "dns-mode"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_RC_MANAGER, "rc-manager"),
	NM_UTILS_FLAGS2STR (NM_CONFIG_CHANGE_GLOBAL_DNS_CONFIG, "global-dns-config"),
);

static void
_set_config_data (NMConfig *self, NMConfigData *new_data, NMConfigChangeFlags reload_flags)
{
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (self);
	NMConfigData *old_data = priv->config_data;
	NMConfigChangeFlags changes, changes_diff;
	gboolean had_new_data = !!new_data;

	nm_assert (reload_flags);
	nm_assert (!NM_FLAGS_ANY (reload_flags, ~NM_CONFIG_CHANGE_CAUSES));
	nm_assert (   NM_IN_SET (reload_flags, NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT, NM_CONFIG_CHANGE_CAUSE_SET_VALUES)
	           || !NM_FLAGS_ANY (reload_flags, NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT | NM_CONFIG_CHANGE_CAUSE_SET_VALUES));

	changes = reload_flags;

	if (new_data) {
		changes_diff = nm_config_data_diff (old_data, new_data);
		if (changes_diff == NM_CONFIG_CHANGE_NONE)
			g_clear_object (&new_data);
		else
			changes |= changes_diff;
	}

	if (   NM_IN_SET (reload_flags,
	                  NM_CONFIG_CHANGE_CAUSE_NO_AUTO_DEFAULT,
	                  NM_CONFIG_CHANGE_CAUSE_SET_VALUES,
	                  NM_CONFIG_CHANGE_CAUSE_CONF)
	    && !new_data) {
		/* no relevant changes that should be propagated. Return silently. */
		return;
	}

	if (new_data) {
		_LOGI ("signal: %s (%s)",
		       nm_config_change_flags_to_string (changes, NULL, 0),
		       nm_config_data_get_config_description (new_data));
		nm_config_data_log (new_data, "CONFIG: ", "  ", priv->no_auto_default_file, NULL);
		priv->config_data = new_data;
	} else if (had_new_data)
		_LOGI ("signal: %s (no changes from disk)", nm_config_change_flags_to_string (changes, NULL, 0));
	else
		_LOGI ("signal: %s", nm_config_change_flags_to_string (changes, NULL, 0));
	g_signal_emit (self, signals[SIGNAL_CONFIG_CHANGED], 0,
	               new_data ?: old_data,
	               changes, old_data);
	if (new_data)
		g_object_unref (old_data);
}

NM_DEFINE_SINGLETON_REGISTER (NMConfig);

NMConfig *
nm_config_get (void)
{
	g_assert (singleton_instance);
	return singleton_instance;
}

NMConfig *
nm_config_setup (const NMConfigCmdLineOptions *cli, char **atomic_section_prefixes, GError **error)
{
	g_assert (!singleton_instance);

	singleton_instance = nm_config_new (cli, atomic_section_prefixes, error);
	if (singleton_instance) {
		nm_singleton_instance_register ();

		/* usually, you would not see this logging line because when creating the
		 * NMConfig instance, the logging is not yet set up to print debug message. */
		nm_log_dbg (LOGD_CORE, "setup %s singleton ("NM_HASH_OBFUSCATE_PTR_FMT")",
		            "NMConfig", NM_HASH_OBFUSCATE_PTR (singleton_instance));
	}
	return singleton_instance;
}

/*****************************************************************************/

static void
set_property (GObject *object, guint prop_id,
              const GValue *value, GParamSpec *pspec)
{
	NMConfig *self = NM_CONFIG (object);
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (self);
	NMConfigCmdLineOptions *cli;
	char **strv;

	switch (prop_id) {
	case PROP_CMD_LINE_OPTIONS:
		/* construct-only */
		cli = g_value_get_pointer (value);
		if (!cli)
			_nm_config_cmd_line_options_clear (&priv->cli);
		else
			_nm_config_cmd_line_options_copy (cli, &priv->cli);
		break;
	case PROP_ATOMIC_SECTION_PREFIXES:
		/* construct-only */
		strv = g_value_get_boxed (value);
		if (strv && !strv[0])
			strv = NULL;
		priv->atomic_section_prefixes = g_strdupv (strv);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

/*****************************************************************************/

static gboolean
init_sync (GInitable *initable, GCancellable *cancellable, GError **error)
{
	NMConfig *self = NM_CONFIG (initable);
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (self);
	gs_unref_keyfile GKeyFile *keyfile = NULL;
	gs_unref_keyfile GKeyFile *keyfile_intern = NULL;
	gs_free char *config_main_file = NULL;
	gs_free char *config_description = NULL;
	gs_strfreev char **no_auto_default = NULL;
	gs_unref_ptrarray GPtrArray *warnings = NULL;
	gs_free char *configure_and_quit = NULL;
	gboolean intern_config_needs_rewrite;
	const char *s;

	if (priv->config_dir) {
		/* Object is already initialized. */
		if (priv->config_data)
			return TRUE;
		g_set_error (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND, "unspecified error");
		g_return_val_if_reached (FALSE);
	}

	s = priv->cli.config_dir ?: ""DEFAULT_CONFIG_DIR;
	priv->config_dir = g_strdup (s[0] == '/' ? s : "");

	s = priv->cli.system_config_dir ?: ""DEFAULT_SYSTEM_CONFIG_DIR;
	if (   s[0] != '/'
	    || nm_streq (s, priv->config_dir))
		s = "";
	priv->system_config_dir = g_strdup (s);

	if (priv->cli.intern_config_file)
		priv->intern_config_file = g_strdup (priv->cli.intern_config_file);
	else
		priv->intern_config_file = g_strdup (DEFAULT_INTERN_CONFIG_FILE);

	warnings = g_ptr_array_new_with_free_func (g_free);

	keyfile = read_entire_config (&priv->cli,
	                              priv->config_dir,
	                              priv->system_config_dir,
	                              &config_main_file,
	                              &config_description,
	                              warnings,
	                              error);
	if (!keyfile)
		return FALSE;

	/* Initialize read-only private members */

	if (priv->cli.no_auto_default_file)
		priv->no_auto_default_file = g_strdup (priv->cli.no_auto_default_file);
	else
		priv->no_auto_default_file = g_strdup (DEFAULT_NO_AUTO_DEFAULT_FILE);

	priv->log_level = nm_strstrip (g_key_file_get_string (keyfile,
	                                                      NM_CONFIG_KEYFILE_GROUP_LOGGING,
	                                                      NM_CONFIG_KEYFILE_KEY_LOGGING_LEVEL,
	                                                      NULL));
	priv->log_domains = nm_strstrip (g_key_file_get_string (keyfile,
	                                                        NM_CONFIG_KEYFILE_GROUP_LOGGING,
	                                                        NM_CONFIG_KEYFILE_KEY_LOGGING_DOMAINS,
	                                                        NULL));
	configure_and_quit = nm_strstrip (g_key_file_get_string (keyfile,
	                                                         NM_CONFIG_KEYFILE_GROUP_MAIN,
	                                                         NM_CONFIG_KEYFILE_KEY_MAIN_CONFIGURE_AND_QUIT,
	                                                         NULL));
	priv->configure_and_quit = string_to_configure_and_quit (configure_and_quit, error);
	if (priv->configure_and_quit == NM_CONFIG_CONFIGURE_AND_QUIT_INVALID)
		return FALSE;

	no_auto_default = no_auto_default_from_file (priv->no_auto_default_file);

	keyfile_intern = intern_config_read (priv->intern_config_file,
	                                     keyfile,
	                                     (const char *const*) priv->atomic_section_prefixes,
	                                     &intern_config_needs_rewrite);
	if (   intern_config_needs_rewrite
	    && priv->configure_and_quit == NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED) {
		intern_config_write (priv->intern_config_file, keyfile_intern, keyfile,
		                     (const char *const*) priv->atomic_section_prefixes, NULL);
	}

	priv->config_data_orig = nm_config_data_new (config_main_file,
	                                             config_description,
	                                             (const char *const*) no_auto_default,
	                                             keyfile,
	                                             keyfile_intern);

	nm_config_data_get_warnings (priv->config_data_orig, warnings);

	priv->config_data = g_object_ref (priv->config_data_orig);
	if (warnings->len > 0) {
		g_ptr_array_add (warnings, NULL);
		priv->warnings = (char **) g_ptr_array_free (g_steal_pointer (&warnings), FALSE);
	}
	return TRUE;
}

/*****************************************************************************/

static void
nm_config_init (NMConfig *config)
{
}

NMConfig *
nm_config_new (const NMConfigCmdLineOptions *cli, char **atomic_section_prefixes, GError **error)
{
	return NM_CONFIG (g_initable_new (NM_TYPE_CONFIG,
	                                  NULL,
	                                  error,
	                                  NM_CONFIG_CMD_LINE_OPTIONS, cli,
	                                  NM_CONFIG_ATOMIC_SECTION_PREFIXES, atomic_section_prefixes,
	                                  NULL));
}

static void
finalize (GObject *gobject)
{
	NMConfigPrivate *priv = NM_CONFIG_GET_PRIVATE (gobject);

	state_free (priv->state);

	g_free (priv->config_dir);
	g_free (priv->system_config_dir);
	g_free (priv->no_auto_default_file);
	g_free (priv->intern_config_file);
	g_free (priv->log_level);
	g_free (priv->log_domains);
	g_strfreev (priv->atomic_section_prefixes);
	g_strfreev (priv->warnings);

	_nm_config_cmd_line_options_clear (&priv->cli);

	g_clear_object (&priv->config_data);
	g_clear_object (&priv->config_data_orig);

	G_OBJECT_CLASS (nm_config_parent_class)->finalize (gobject);
}

static void
nm_config_class_init (NMConfigClass *config_class)
{
	GObjectClass *object_class = G_OBJECT_CLASS (config_class);

	object_class->finalize = finalize;
	object_class->set_property = set_property;

	obj_properties[PROP_CMD_LINE_OPTIONS] =
	     g_param_spec_pointer (NM_CONFIG_CMD_LINE_OPTIONS, "", "",
	                           G_PARAM_WRITABLE |
	                           G_PARAM_CONSTRUCT_ONLY |
	                           G_PARAM_STATIC_STRINGS);

	obj_properties[PROP_ATOMIC_SECTION_PREFIXES] =
	     g_param_spec_boxed (NM_CONFIG_ATOMIC_SECTION_PREFIXES, "", "",
	                         G_TYPE_STRV,
	                         G_PARAM_WRITABLE |
	                         G_PARAM_CONSTRUCT_ONLY |
	                         G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);

	signals[SIGNAL_CONFIG_CHANGED] =
	    g_signal_new (NM_CONFIG_SIGNAL_CONFIG_CHANGED,
	                  G_OBJECT_CLASS_TYPE (object_class),
	                  G_SIGNAL_RUN_FIRST,
	                  0,
	                  NULL, NULL, NULL,
	                  G_TYPE_NONE,
	                  3,
	                  NM_TYPE_CONFIG_DATA,
	                  /* Use plain guint type for changes argument. This avoids
	                   * glib/ffi bug https://bugzilla.redhat.com/show_bug.cgi?id=1260577 */
	                  /* NM_TYPE_CONFIG_CHANGE_FLAGS, */
	                  G_TYPE_UINT,
	                  NM_TYPE_CONFIG_DATA);

	G_STATIC_ASSERT_EXPR (sizeof (guint) == sizeof (NMConfigChangeFlags));
	G_STATIC_ASSERT_EXPR (((gint64) ((NMConfigChangeFlags) -1)) > ((gint64) 0));
}

static void
nm_config_initable_iface_init (GInitableIface *iface)
{
	iface->init = init_sync;
}
